diff --git a/pkg/provisioner/ironic/update_opts.go b/pkg/provisioner/ironic/clients/updateopts.go similarity index 81% rename from pkg/provisioner/ironic/update_opts.go rename to pkg/provisioner/ironic/clients/updateopts.go index cdb14d80f1..e4bd537c5e 100644 --- a/pkg/provisioner/ironic/update_opts.go +++ b/pkg/provisioner/ironic/clients/updateopts.go @@ -1,4 +1,4 @@ -package ironic +package clients import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" ) -type optionsData map[string]interface{} +type UpdateOptsData map[string]interface{} func optionValueEqual(current, value interface{}) bool { if reflect.DeepEqual(current, value) { @@ -125,18 +125,18 @@ func getUpdateOperation(name string, currentData map[string]interface{}, desired return nil } -type nodeUpdater struct { +type NodeUpdater struct { Updates nodes.UpdateOpts log logr.Logger } -func updateOptsBuilder(logger logr.Logger) *nodeUpdater { - return &nodeUpdater{ +func UpdateOptsBuilder(logger logr.Logger) *NodeUpdater { + return &NodeUpdater{ log: logger, } } -func (nu *nodeUpdater) logger(basepath, option string) logr.Logger { +func (nu *NodeUpdater) logger(basepath, option string) logr.Logger { log := nu.log.WithValues("option", option) if basepath != "" { log = log.WithValues("section", basepath[1:]) @@ -144,11 +144,11 @@ func (nu *nodeUpdater) logger(basepath, option string) logr.Logger { return log } -func (nu *nodeUpdater) path(basepath, option string) string { +func (nu *NodeUpdater) path(basepath, option string) string { return fmt.Sprintf("%s/%s", basepath, option) } -func (nu *nodeUpdater) setSectionUpdateOpts(currentData map[string]interface{}, settings optionsData, basepath string) { +func (nu *NodeUpdater) setSectionUpdateOpts(currentData map[string]interface{}, settings UpdateOptsData, basepath string) { for name, desiredValue := range settings { updateOp := getUpdateOperation(name, currentData, desiredValue, nu.path(basepath, name), nu.logger(basepath, name)) @@ -158,25 +158,25 @@ func (nu *nodeUpdater) setSectionUpdateOpts(currentData map[string]interface{}, } } -func (nu *nodeUpdater) SetTopLevelOpt(name string, desiredValue, currentValue interface{}) *nodeUpdater { +func (nu *NodeUpdater) SetTopLevelOpt(name string, desiredValue, currentValue interface{}) *NodeUpdater { currentData := map[string]interface{}{name: currentValue} - desiredData := optionsData{name: desiredValue} + desiredData := UpdateOptsData{name: desiredValue} nu.setSectionUpdateOpts(currentData, desiredData, "") return nu } -func (nu *nodeUpdater) SetPropertiesOpts(settings optionsData, node *nodes.Node) *nodeUpdater { +func (nu *NodeUpdater) SetPropertiesOpts(settings UpdateOptsData, node *nodes.Node) *NodeUpdater { nu.setSectionUpdateOpts(node.Properties, settings, "/properties") return nu } -func (nu *nodeUpdater) SetInstanceInfoOpts(settings optionsData, node *nodes.Node) *nodeUpdater { +func (nu *NodeUpdater) SetInstanceInfoOpts(settings UpdateOptsData, node *nodes.Node) *NodeUpdater { nu.setSectionUpdateOpts(node.InstanceInfo, settings, "/instance_info") return nu } -func (nu *nodeUpdater) SetDriverInfoOpts(settings optionsData, node *nodes.Node) *nodeUpdater { +func (nu *NodeUpdater) SetDriverInfoOpts(settings UpdateOptsData, node *nodes.Node) *NodeUpdater { nu.setSectionUpdateOpts(node.DriverInfo, settings, "/driver_info") return nu } diff --git a/pkg/provisioner/ironic/clients/updateopts_test.go b/pkg/provisioner/ironic/clients/updateopts_test.go new file mode 100644 index 0000000000..a51f78c21d --- /dev/null +++ b/pkg/provisioner/ironic/clients/updateopts_test.go @@ -0,0 +1,470 @@ +package clients + +import ( + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" + "github.com/stretchr/testify/assert" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +func TestOptionValueEqual(t *testing.T) { + cases := []struct { + Name string + Before interface{} + After interface{} + Equal bool + }{ + { + Name: "nil interface", + Before: nil, + After: "foo", + Equal: false, + }, + { + Name: "equal string", + Before: "foo", + After: "foo", + Equal: true, + }, + { + Name: "unequal string", + Before: "foo", + After: "bar", + Equal: false, + }, + { + Name: "equal true", + Before: true, + After: true, + Equal: true, + }, + { + Name: "equal false", + Before: false, + After: false, + Equal: true, + }, + { + Name: "unequal true", + Before: true, + After: false, + Equal: false, + }, + { + Name: "unequal false", + Before: false, + After: true, + Equal: false, + }, + { + Name: "equal int", + Before: 42, + After: 42, + Equal: true, + }, + { + Name: "unequal int", + Before: 27, + After: 42, + Equal: false, + }, + { + Name: "string int", + Before: "42", + After: 42, + Equal: false, + }, + { + Name: "int string", + Before: 42, + After: "42", + Equal: false, + }, + { + Name: "bool int", + Before: false, + After: 0, + Equal: false, + }, + { + Name: "int bool", + Before: 1, + After: true, + Equal: false, + }, + { + Name: "string map", + Before: "foo", + After: map[string]string{"foo": "foo"}, + Equal: false, + }, + { + Name: "map string", + Before: map[string]interface{}{"foo": "foo"}, + After: "foo", + Equal: false, + }, + { + Name: "string list", + Before: "foo", + After: []string{"foo"}, + Equal: false, + }, + { + Name: "list string", + Before: []string{"foo"}, + After: "foo", + Equal: false, + }, + { + Name: "map list", + Before: map[string]interface{}{"foo": "foo"}, + After: []string{"foo"}, + Equal: false, + }, + { + Name: "list map", + Before: []string{"foo"}, + After: map[string]string{"foo": "foo"}, + Equal: false, + }, + { + Name: "equal map string-typed", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]string{"foo": "bar"}, + Equal: true, + }, + { + Name: "unequal map string-typed", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]string{"foo": "baz"}, + Equal: false, + }, + { + Name: "equal map int-typed", + Before: map[string]interface{}{"foo": 42}, + After: map[string]int{"foo": 42}, + Equal: true, + }, + { + Name: "unequal map int-typed", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]int{"foo": 42}, + Equal: false, + }, + { + Name: "equal map", + Before: map[string]interface{}{"foo": "bar", "42": 42}, + After: map[string]interface{}{"foo": "bar", "42": 42}, + Equal: true, + }, + { + Name: "unequal map", + Before: map[string]interface{}{"foo": "bar", "42": 42}, + After: map[string]interface{}{"foo": "bar", "42": 27}, + Equal: false, + }, + { + Name: "equal map empty string", + Before: map[string]interface{}{"foo": ""}, + After: map[string]interface{}{"foo": ""}, + Equal: true, + }, + { + Name: "unequal map replace empty string", + Before: map[string]interface{}{"foo": ""}, + After: map[string]interface{}{"foo": "bar"}, + Equal: false, + }, + { + Name: "unequal map replace with empty string", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]interface{}{"foo": ""}, + Equal: false, + }, + { + Name: "shorter map", + Before: map[string]interface{}{"foo": "bar", "42": 42}, + After: map[string]interface{}{"foo": "bar"}, + Equal: false, + }, + { + Name: "longer map", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]interface{}{"foo": "bar", "42": 42}, + Equal: false, + }, + { + Name: "different map", + Before: map[string]interface{}{"foo": "bar"}, + After: map[string]interface{}{"baz": "bar"}, + Equal: false, + }, + { + Name: "equal list string-typed", + Before: []interface{}{"foo", "bar"}, + After: []string{"foo", "bar"}, + Equal: true, + }, + { + Name: "unequal list string-typed", + Before: []interface{}{"foo", "bar"}, + After: []string{"foo", "baz"}, + Equal: false, + }, + { + Name: "equal list", + Before: []interface{}{"foo", 42}, + After: []interface{}{"foo", 42}, + Equal: true, + }, + { + Name: "unequal list", + Before: []interface{}{"foo", 42}, + After: []interface{}{"foo", 27}, + Equal: false, + }, + { + Name: "shorter list", + Before: []interface{}{"foo", 42}, + After: []interface{}{"foo"}, + Equal: false, + }, + { + Name: "longer list", + Before: []interface{}{"foo"}, + After: []interface{}{"foo", 42}, + Equal: false, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + var ne = "=" + if c.Equal { + ne = "!" + } + assert.Equal(t, c.Equal, optionValueEqual(c.Before, c.After), + fmt.Sprintf("%v %s= %v", c.Before, ne, c.After)) + }) + } +} + +func TestGetUpdateOperation(t *testing.T) { + var nilp *string + barExist := "bar" + bar := "bar" + baz := "baz" + existingData := map[string]interface{}{ + "foo": "bar", + "foop": &barExist, + "nil": nilp, + } + cases := []struct { + Name string + Field string + NewValue interface{} + ExpectedOp nodes.UpdateOp + ExpectedValue string + }{ + { + Name: "add value", + Field: "baz", + NewValue: "quux", + ExpectedOp: nodes.AddOp, + }, + { + Name: "add value pointer", + Field: "baz", + NewValue: &bar, + ExpectedValue: bar, + ExpectedOp: nodes.AddOp, + }, + { + Name: "add pointer value pointer", + Field: "nil", + NewValue: &bar, + ExpectedValue: bar, + ExpectedOp: nodes.AddOp, + }, + { + Name: "keep value", + Field: "foo", + NewValue: "bar", + }, + { + Name: "keep pointer value", + Field: "foop", + NewValue: "bar", + }, + { + Name: "keep value pointer", + Field: "foo", + NewValue: &bar, + }, + { + Name: "keep pointer value pointer", + Field: "foop", + NewValue: &bar, + }, + { + Name: "change value", + Field: "foo", + NewValue: "baz", + ExpectedOp: nodes.AddOp, + }, + { + Name: "change pointer value", + Field: "foop", + NewValue: "baz", + ExpectedOp: nodes.AddOp, + }, + { + Name: "change value pointer", + Field: "foo", + NewValue: &baz, + ExpectedValue: baz, + ExpectedOp: nodes.AddOp, + }, + { + Name: "change pointer value pointer", + Field: "foop", + NewValue: &baz, + ExpectedValue: baz, + ExpectedOp: nodes.AddOp, + }, + { + Name: "delete value", + Field: "foo", + NewValue: nil, + ExpectedOp: nodes.RemoveOp, + }, + { + Name: "delete value pointer", + Field: "foo", + NewValue: nilp, + ExpectedOp: nodes.RemoveOp, + }, + { + Name: "nonexistent value", + Field: "bar", + NewValue: nil, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + path := fmt.Sprintf("test/%s", c.Field) + updateOp := getUpdateOperation( + c.Field, existingData, + c.NewValue, + path, logr.Logger{}) + + switch c.ExpectedOp { + case nodes.AddOp, nodes.ReplaceOp: + assert.NotNil(t, updateOp) + ev := c.ExpectedValue + if ev == "" { + ev = c.NewValue.(string) + } + assert.Equal(t, c.ExpectedOp, updateOp.Op) + assert.Equal(t, ev, updateOp.Value) + assert.Equal(t, path, updateOp.Path) + case nodes.RemoveOp: + assert.NotNil(t, updateOp) + assert.Equal(t, c.ExpectedOp, updateOp.Op) + assert.Equal(t, path, updateOp.Path) + default: + assert.Nil(t, updateOp) + } + }) + } +} + +func TestTopLevelUpdateOpt(t *testing.T) { + u := UpdateOptsBuilder(logf.Log) + u.SetTopLevelOpt("foo", "baz", "bar") + ops := u.Updates + assert.Len(t, ops, 1) + op := ops[0].(nodes.UpdateOperation) + assert.Equal(t, nodes.AddOp, op.Op) + assert.Equal(t, "baz", op.Value) + assert.Equal(t, "/foo", op.Path) + + u = UpdateOptsBuilder(logf.Log) + u.SetTopLevelOpt("foo", "bar", "bar") + assert.Len(t, u.Updates, 0) +} + +func TestPropertiesUpdateOpts(t *testing.T) { + newValues := UpdateOptsData{ + "foo": "bar", + "baz": "quux", + } + node := nodes.Node{ + Properties: map[string]interface{}{ + "foo": "bar", + }, + } + + u := UpdateOptsBuilder(logf.Log) + u.SetPropertiesOpts(newValues, &node) + ops := u.Updates + assert.Len(t, ops, 1) + op := ops[0].(nodes.UpdateOperation) + assert.Equal(t, nodes.AddOp, op.Op) + assert.Equal(t, "quux", op.Value) + assert.Equal(t, "/properties/baz", op.Path) +} + +func TestInstanceInfoUpdateOpts(t *testing.T) { + newValues := UpdateOptsData{ + "foo": "bar", + "baz": "quux", + } + node := nodes.Node{ + InstanceInfo: map[string]interface{}{ + "foo": "bar", + }, + } + + u := UpdateOptsBuilder(logf.Log) + u.SetInstanceInfoOpts(newValues, &node) + ops := u.Updates + assert.Len(t, ops, 1) + op := ops[0].(nodes.UpdateOperation) + assert.Equal(t, nodes.AddOp, op.Op) + assert.Equal(t, "quux", op.Value) + assert.Equal(t, "/instance_info/baz", op.Path) +} + +func TestSanitisedValue(t *testing.T) { + unchanged := []interface{}{ + "foo", + 42, + true, + []string{"bar", "baz"}, + map[string]string{"foo": "bar"}, + map[string][]string{"foo": {"bar", "baz"}}, + map[string]interface{}{"foo": []string{"bar", "baz"}, "bar": 42}, + } + + for _, u := range unchanged { + assert.Exactly(t, u, sanitisedValue(u)) + } + + unsafe := map[string]interface{}{ + "foo": "bar", + "password": "secret", + "ipmi_password": "secret", + } + safe := map[string]interface{}{ + "foo": "bar", + "password": "", + "ipmi_password": "", + } + assert.Exactly(t, safe, sanitisedValue(unsafe)) +} diff --git a/pkg/provisioner/ironic/inspecthardware.go b/pkg/provisioner/ironic/inspecthardware.go index c228d8c66e..f9b2f06236 100644 --- a/pkg/provisioner/ironic/inspecthardware.go +++ b/pkg/provisioner/ironic/inspecthardware.go @@ -11,6 +11,7 @@ import ( metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" "github.com/metal3-io/baremetal-operator/pkg/hardwareutils/bmc" "github.com/metal3-io/baremetal-operator/pkg/provisioner" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/hardwaredetails" ) @@ -40,8 +41,8 @@ func (p *ironicProvisioner) abortInspection(ironicNode *nodes.Node) (result prov func (p *ironicProvisioner) startInspection(data provisioner.InspectData, ironicNode *nodes.Node) (result provisioner.Result, started bool, err error) { _, started, result, err = p.tryUpdateNode( ironicNode, - updateOptsBuilder(p.log). - SetPropertiesOpts(optionsData{ + clients.UpdateOptsBuilder(p.log). + SetPropertiesOpts(clients.UpdateOptsData{ "capabilities": buildCapabilitiesValue(ironicNode, data.BootMode), }, ironicNode), ) diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index b206b411c5..bb4741dd7e 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -319,7 +319,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(data provisioner.Management } var ironicNode *nodes.Node - updater := updateOptsBuilder(p.log) + updater := clients.UpdateOptsBuilder(p.log) p.debugLog.Info("validating management access") @@ -561,7 +561,7 @@ func (p *ironicProvisioner) ValidateManagementAccess(data provisioner.Management } func (p *ironicProvisioner) configureImages(data provisioner.ManagementAccessData, ironicNode *nodes.Node, bmcAccess bmc.AccessDetails) (result provisioner.Result, err error) { - updater := updateOptsBuilder(p.log) + updater := clients.UpdateOptsBuilder(p.log) deployImageInfo := setDeployImage(p.config, bmcAccess, data.PreprovisioningImage) updater.SetDriverInfoOpts(deployImageInfo, ironicNode) @@ -678,8 +678,8 @@ func setExternalURL(p *ironicProvisioner, driverInfo map[string]interface{}) map return driverInfo } -func setDeployImage(config ironicConfig, accessDetails bmc.AccessDetails, hostImage *provisioner.PreprovisioningImage) optionsData { - deployImageInfo := optionsData{ +func setDeployImage(config ironicConfig, accessDetails bmc.AccessDetails, hostImage *provisioner.PreprovisioningImage) clients.UpdateOptsData { + deployImageInfo := clients.UpdateOptsData{ deployKernelKey: nil, deployRamdiskKey: nil, deployISOKey: nil, @@ -727,7 +727,7 @@ func setDeployImage(config ironicConfig, accessDetails bmc.AccessDetails, hostIm return nil } -func (p *ironicProvisioner) tryUpdateNode(ironicNode *nodes.Node, updater *nodeUpdater) (updatedNode *nodes.Node, success bool, result provisioner.Result, err error) { +func (p *ironicProvisioner) tryUpdateNode(ironicNode *nodes.Node, updater *clients.NodeUpdater) (updatedNode *nodes.Node, success bool, result provisioner.Result, err error) { if len(updater.Updates) == 0 { updatedNode = ironicNode success = true @@ -812,8 +812,8 @@ func (p *ironicProvisioner) UpdateHardwareState() (hwState provisioner.HardwareS return } -func (p *ironicProvisioner) setLiveIsoUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *nodeUpdater) { - optValues := optionsData{ +func (p *ironicProvisioner) setLiveIsoUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *clients.NodeUpdater) { + optValues := clients.UpdateOptsData{ "boot_iso": imageData.URL, // remove any image_source or checksum options @@ -826,23 +826,23 @@ func (p *ironicProvisioner) setLiveIsoUpdateOptsForNode(ironicNode *nodes.Node, SetInstanceInfoOpts(optValues, ironicNode). SetTopLevelOpt("deploy_interface", "ramdisk", ironicNode.DeployInterface) - driverOptValues := optionsData{"force_persistent_boot_device": "Default"} + driverOptValues := clients.UpdateOptsData{"force_persistent_boot_device": "Default"} if p.config.liveISOForcePersistentBootDevice != "" { - driverOptValues = optionsData{ + driverOptValues = clients.UpdateOptsData{ "force_persistent_boot_device": p.config.liveISOForcePersistentBootDevice, } } updater.SetDriverInfoOpts(driverOptValues, ironicNode) } -func (p *ironicProvisioner) setDirectDeployUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *nodeUpdater) { +func (p *ironicProvisioner) setDirectDeployUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *clients.NodeUpdater) { checksum, checksumType, ok := imageData.GetChecksum() if !ok { p.log.Info("image/checksum not found for host") return } - optValues := optionsData{ + optValues := clients.UpdateOptsData{ // Remove any boot_iso field "boot_iso": nil, "image_source": imageData.URL, @@ -865,19 +865,19 @@ func (p *ironicProvisioner) setDirectDeployUpdateOptsForNode(ironicNode *nodes.N updater.SetTopLevelOpt("deploy_interface", nil, ironicNode.DeployInterface) } - driverOptValues := optionsData{ + driverOptValues := clients.UpdateOptsData{ "force_persistent_boot_device": "Default", } updater.SetDriverInfoOpts(driverOptValues, ironicNode) } -func (p *ironicProvisioner) setCustomDeployUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *nodeUpdater) { - var optValues optionsData +func (p *ironicProvisioner) setCustomDeployUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, updater *clients.NodeUpdater) { + var optValues clients.UpdateOptsData if imageData != nil && imageData.URL != "" { checksum, checksumType, ok := imageData.GetChecksum() // NOTE(dtantsur): all fields are optional for custom deploy if ok { - optValues = optionsData{ + optValues = clients.UpdateOptsData{ "boot_iso": nil, "image_checksum": nil, "image_source": imageData.URL, @@ -886,7 +886,7 @@ func (p *ironicProvisioner) setCustomDeployUpdateOptsForNode(ironicNode *nodes.N "image_disk_format": imageData.DiskFormat, } } else { - optValues = optionsData{ + optValues = clients.UpdateOptsData{ "boot_iso": nil, "image_checksum": nil, "image_source": imageData.URL, @@ -897,7 +897,7 @@ func (p *ironicProvisioner) setCustomDeployUpdateOptsForNode(ironicNode *nodes.N } } else { // Clean up everything - optValues = optionsData{ + optValues = clients.UpdateOptsData{ "boot_iso": nil, "image_checksum": nil, "image_source": nil, @@ -912,11 +912,11 @@ func (p *ironicProvisioner) setCustomDeployUpdateOptsForNode(ironicNode *nodes.N SetTopLevelOpt("deploy_interface", "custom-agent", ironicNode.DeployInterface) } -func (p *ironicProvisioner) getImageUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, bootMode metal3api.BootMode, hasCustomDeploy bool, updater *nodeUpdater) { +func (p *ironicProvisioner) getImageUpdateOptsForNode(ironicNode *nodes.Node, imageData *metal3api.Image, bootMode metal3api.BootMode, hasCustomDeploy bool, updater *clients.NodeUpdater) { // instance_uuid updater.SetTopLevelOpt("instance_uuid", string(p.objectMeta.UID), ironicNode.InstanceUUID) - updater.SetInstanceInfoOpts(optionsData{ + updater.SetInstanceInfoOpts(clients.UpdateOptsData{ "capabilities": buildInstanceInfoCapabilities(bootMode), }, ironicNode) @@ -932,13 +932,13 @@ func (p *ironicProvisioner) getImageUpdateOptsForNode(ironicNode *nodes.Node, im } } -func (p *ironicProvisioner) getUpdateOptsForNode(ironicNode *nodes.Node, data provisioner.ProvisionData) *nodeUpdater { - updater := updateOptsBuilder(p.log) +func (p *ironicProvisioner) getUpdateOptsForNode(ironicNode *nodes.Node, data provisioner.ProvisionData) *clients.NodeUpdater { + updater := clients.UpdateOptsBuilder(p.log) hasCustomDeploy := data.CustomDeploy != nil && data.CustomDeploy.Method != "" p.getImageUpdateOptsForNode(ironicNode, &data.Image, data.BootMode, hasCustomDeploy, updater) - opts := optionsData{ + opts := clients.UpdateOptsData{ "root_device": devicehints.MakeHintMap(data.RootDeviceHints), "capabilities": buildCapabilitiesValue(ironicNode, data.BootMode), } @@ -1731,7 +1731,7 @@ func (p *ironicProvisioner) Delete() (result provisioner.Result, err error) { // Make sure we don't have a stale instance UUID if ironicNode.InstanceUUID != "" { p.log.Info("removing stale instance UUID before deletion", "instanceUUID", ironicNode.InstanceUUID) - updater := updateOptsBuilder(p.log) + updater := clients.UpdateOptsBuilder(p.log) updater.SetTopLevelOpt("instance_uuid", nil, ironicNode.InstanceUUID) _, success, result, err := p.tryUpdateNode(ironicNode, updater) if !success { diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go index ccc242f29b..a85e28fabc 100644 --- a/pkg/provisioner/ironic/provision_test.go +++ b/pkg/provisioner/ironic/provision_test.go @@ -1,12 +1,14 @@ package ironic import ( + "fmt" "net/url" "testing" "time" "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" + "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1/profile" "github.com/metal3-io/baremetal-operator/pkg/hardwareutils/bmc" "github.com/metal3-io/baremetal-operator/pkg/provisioner" "github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture" @@ -14,7 +16,9 @@ import ( "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testbmc" "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) func TestProvision(t *testing.T) { @@ -691,3 +695,752 @@ func TestBuildCleanSteps(t *testing.T) { }) } } + +func TestGetUpdateOptsForNodeWithRootHints(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHost() + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{} + + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + RootDeviceHints: host.Status.Provisioning.RootDeviceHints, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Map map[string]string // Expected roothdevicehint map + Value interface{} // the value being passed to ironic (or value associated with the key) + }{ + { + Path: "/properties/root_device", + Value: "userdefined_devicename", + Map: map[string]string{ + "name": "s== userd_devicename", + "hctl": "s== 1:2:3:4", + "model": " userd_model", + "vendor": " userd_vendor", + "serial": "s== userd_serial", + "size": ">= 40", + "wwn": "s== userd_wwn", + "wwn_with_extension": "s== userd_with_extension", + "wwn_vendor_extension": "s== userd_vendor_extension", + "rotational": "true", + }, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + if e.Map != nil { + assert.Equal(t, e.Map, update.Value, fmt.Sprintf("%s does not match", e.Path)) + } else { + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + } + }) + } +} + +func TestGetUpdateOptsForNodeVirtual(t *testing.T) { + host := metal3api.BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + Spec: metal3api.BareMetalHostSpec{ + BMC: metal3api.BMCDetails{ + Address: "test://test.bmc/", + }, + Image: &metal3api.Image{ + URL: "not-empty", + Checksum: "checksum", + ChecksumType: metal3api.MD5, + DiskFormat: ptr.To("raw"), + }, + Online: true, + HardwareProfile: "unknown", + }, + Status: metal3api.BareMetalHostStatus{ + HardwareProfile: "libvirt", + Provisioning: metal3api.ProvisionStatus{ + ID: "provisioning-id", + }, + }, + } + + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(fmt.Errorf("could not create provisioner: %w", err)) + } + ironicNode := &nodes.Node{} + + hwProf, _ := profile.GetProfile("libvirt") + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + HardwareProfile: hwProf, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + }{ + { + Path: "/instance_info/image_source", + Value: "not-empty", + }, + { + Path: "/instance_info/image_os_hash_algo", + Value: "md5", + }, + { + Path: "/instance_info/image_os_hash_value", + Value: "checksum", + }, + { + Path: "/instance_info/image_disk_format", + Value: "raw", + }, + { + Path: "/instance_info/capabilities", + Value: map[string]string{}, + }, + { + Path: "/instance_uuid", + Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeDell(t *testing.T) { + host := metal3api.BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + Spec: metal3api.BareMetalHostSpec{ + BMC: metal3api.BMCDetails{ + Address: "test://test.bmc/", + }, + Image: &metal3api.Image{ + URL: "not-empty", + Checksum: "checksum", + ChecksumType: metal3api.MD5, + // DiskFormat not given to verify it is not added in instance_info + }, + Online: true, + }, + Status: metal3api.BareMetalHostStatus{ + HardwareProfile: "dell", + Provisioning: metal3api.ProvisionStatus{ + ID: "provisioning-id", + }, + }, + } + + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{} + + hwProf, _ := profile.GetProfile("dell") + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + HardwareProfile: hwProf, + CPUArchitecture: "x86_64", + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + }{ + { + Path: "/instance_info/image_source", + Value: "not-empty", + }, + { + Path: "/instance_info/image_os_hash_algo", + Value: "md5", + }, + { + Path: "/instance_info/image_os_hash_value", + Value: "checksum", + }, + { + Path: "/instance_uuid", + Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + { + Path: "/properties/cpu_arch", + Value: "x86_64", + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeLiveIso(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHostLiveIso() + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{} + + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/instance_info/boot_iso", + Value: "not-empty", + Op: nodes.AddOp, + }, + { + Path: "/instance_info/capabilities", + Value: map[string]string{}, + }, + { + Path: "/deploy_interface", + Value: "ramdisk", + Op: nodes.AddOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeImageToLiveIso(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHostLiveIso() + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{ + InstanceInfo: map[string]interface{}{ + "image_source": "oldimage", + "image_os_hash_value": "thechecksum", + "image_os_hash_algo": "md5", + }, + } + + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/instance_info/boot_iso", + Value: "not-empty", + Op: nodes.AddOp, + }, + { + Path: "/deploy_interface", + Value: "ramdisk", + Op: nodes.AddOp, + }, + { + Path: "/instance_info/image_source", + Op: nodes.RemoveOp, + }, + { + Path: "/instance_info/image_os_hash_algo", + Op: nodes.RemoveOp, + }, + { + Path: "/instance_info/image_os_hash_value", + Op: nodes.RemoveOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) + assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeLiveIsoToImage(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHost() + host.Spec.Image.URL = "newimage" + host.Spec.Image.Checksum = "thechecksum" + host.Spec.Image.ChecksumType = metal3api.MD5 + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{ + InstanceInfo: map[string]interface{}{ + "boot_iso": "oldimage", + }, + DeployInterface: "ramdisk", + } + + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/instance_info/boot_iso", + Op: nodes.RemoveOp, + }, + { + Path: "/deploy_interface", + Op: nodes.RemoveOp, + }, + { + Path: "/instance_info/image_source", + Value: "newimage", + Op: nodes.AddOp, + }, + { + Path: "/instance_info/image_os_hash_algo", + Value: "md5", + Op: nodes.AddOp, + }, + { + Path: "/instance_info/image_os_hash_value", + Value: "thechecksum", + Op: nodes.AddOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) + assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeCustomDeploy(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHostCustomDeploy(true) + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{} + + provData := provisioner.ProvisionData{ + Image: metal3api.Image{}, + BootMode: metal3api.DefaultBootMode, + CustomDeploy: host.Spec.CustomDeploy, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/instance_uuid", + Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + { + Path: "/deploy_interface", + Value: "custom-agent", + Op: nodes.AddOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeCustomDeployWithImage(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHostCustomDeploy(false) + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{} + + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.DefaultBootMode, + CustomDeploy: host.Spec.CustomDeploy, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/instance_info/image_source", + Value: "not-empty", + }, + { + Path: "/instance_uuid", + Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + { + Path: "/deploy_interface", + Value: "custom-agent", + Op: nodes.AddOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeImageToCustomDeploy(t *testing.T) { + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + host := makeHostCustomDeploy(false) + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(err) + } + ironicNode := &nodes.Node{ + InstanceInfo: map[string]interface{}{ + "image_source": "oldimage", + "image_os_hash_value": "thechecksum", + "image_os_hash_algo": "md5", + }, + } + + provData := provisioner.ProvisionData{ + Image: metal3api.Image{}, + BootMode: metal3api.DefaultBootMode, + CustomDeploy: host.Spec.CustomDeploy, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + Op nodes.UpdateOp // The operation add/replace/remove + }{ + { + Path: "/deploy_interface", + Value: "custom-agent", + Op: nodes.AddOp, + }, + { + Path: "/instance_info/image_source", + Op: nodes.RemoveOp, + }, + { + Path: "/instance_info/image_os_hash_algo", + Op: nodes.RemoveOp, + }, + { + Path: "/instance_info/image_os_hash_value", + Op: nodes.RemoveOp, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) + assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) + }) + } +} + +func TestGetUpdateOptsForNodeSecureBoot(t *testing.T) { + host := metal3api.BareMetalHost{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myhost", + Namespace: "myns", + UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", + }, + Spec: metal3api.BareMetalHostSpec{ + BMC: metal3api.BMCDetails{ + Address: "test://test.bmc/", + }, + Image: &metal3api.Image{ + URL: "not-empty", + Checksum: "checksum", + ChecksumType: metal3api.MD5, + DiskFormat: ptr.To("raw"), + }, + Online: true, + HardwareProfile: "unknown", + }, + Status: metal3api.BareMetalHostStatus{ + HardwareProfile: "libvirt", + Provisioning: metal3api.ProvisionStatus{ + ID: "provisioning-id", + }, + }, + } + + eventPublisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) + if err != nil { + t.Fatal(fmt.Errorf("could not create provisioner: %w", err)) + } + ironicNode := &nodes.Node{} + + hwProf, _ := profile.GetProfile("libvirt") + provData := provisioner.ProvisionData{ + Image: *host.Spec.Image, + BootMode: metal3api.UEFISecureBoot, + HardwareProfile: hwProf, + } + patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates + + t.Logf("patches: %v", patches) + + expected := []struct { + Path string // the node property path + Key string // if value is a map, the key we care about + Value interface{} // the value being passed to ironic (or value associated with the key) + }{ + { + Path: "/instance_info/image_source", + Value: "not-empty", + }, + { + Path: "/instance_info/capabilities", + Value: map[string]string{ + "secure_boot": "true", + }, + }, + } + + for _, e := range expected { + t.Run(e.Path, func(t *testing.T) { + t.Logf("expected: %v", e) + var update nodes.UpdateOperation + for _, patch := range patches { + update = patch.(nodes.UpdateOperation) + if update.Path == e.Path { + break + } + } + if update.Path != e.Path { + t.Errorf("did not find %q in updates", e.Path) + return + } + t.Logf("update: %v", update) + assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) + }) + } +} diff --git a/pkg/provisioner/ironic/raid.go b/pkg/provisioner/ironic/raid.go index 67476f7616..232e985700 100644 --- a/pkg/provisioner/ironic/raid.go +++ b/pkg/provisioner/ironic/raid.go @@ -7,6 +7,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" "github.com/metal3-io/baremetal-operator/pkg/provisioner" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/devicehints" "github.com/pkg/errors" ) @@ -42,7 +43,7 @@ func setTargetRAIDCfg(p *ironicProvisioner, raidInterface string, ironicNode *no p.log.Info("rootDeviceHints is used, the first volume of raid will not be set to root") } - updater := updateOptsBuilder(p.log) + updater := clients.UpdateOptsBuilder(p.log) updater.SetTopLevelOpt("raid_interface", targetRaidInterface, ironicNode.RAIDInterface) ironicNode, success, result, err := p.tryUpdateNode(ironicNode, updater) if !success { diff --git a/pkg/provisioner/ironic/updateopts_test.go b/pkg/provisioner/ironic/updateopts_test.go deleted file mode 100644 index a2a92dee9f..0000000000 --- a/pkg/provisioner/ironic/updateopts_test.go +++ /dev/null @@ -1,1227 +0,0 @@ -package ironic - -import ( - "fmt" - "testing" - - "github.com/go-logr/logr" - "github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes" - metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" - "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1/profile" - "github.com/metal3-io/baremetal-operator/pkg/hardwareutils/bmc" - "github.com/metal3-io/baremetal-operator/pkg/provisioner" - "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/pointer" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -func TestOptionValueEqual(t *testing.T) { - cases := []struct { - Name string - Before interface{} - After interface{} - Equal bool - }{ - { - Name: "nil interface", - Before: nil, - After: "foo", - Equal: false, - }, - { - Name: "equal string", - Before: "foo", - After: "foo", - Equal: true, - }, - { - Name: "unequal string", - Before: "foo", - After: "bar", - Equal: false, - }, - { - Name: "equal true", - Before: true, - After: true, - Equal: true, - }, - { - Name: "equal false", - Before: false, - After: false, - Equal: true, - }, - { - Name: "unequal true", - Before: true, - After: false, - Equal: false, - }, - { - Name: "unequal false", - Before: false, - After: true, - Equal: false, - }, - { - Name: "equal int", - Before: 42, - After: 42, - Equal: true, - }, - { - Name: "unequal int", - Before: 27, - After: 42, - Equal: false, - }, - { - Name: "string int", - Before: "42", - After: 42, - Equal: false, - }, - { - Name: "int string", - Before: 42, - After: "42", - Equal: false, - }, - { - Name: "bool int", - Before: false, - After: 0, - Equal: false, - }, - { - Name: "int bool", - Before: 1, - After: true, - Equal: false, - }, - { - Name: "string map", - Before: "foo", - After: map[string]string{"foo": "foo"}, - Equal: false, - }, - { - Name: "map string", - Before: map[string]interface{}{"foo": "foo"}, - After: "foo", - Equal: false, - }, - { - Name: "string list", - Before: "foo", - After: []string{"foo"}, - Equal: false, - }, - { - Name: "list string", - Before: []string{"foo"}, - After: "foo", - Equal: false, - }, - { - Name: "map list", - Before: map[string]interface{}{"foo": "foo"}, - After: []string{"foo"}, - Equal: false, - }, - { - Name: "list map", - Before: []string{"foo"}, - After: map[string]string{"foo": "foo"}, - Equal: false, - }, - { - Name: "equal map string-typed", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]string{"foo": "bar"}, - Equal: true, - }, - { - Name: "unequal map string-typed", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]string{"foo": "baz"}, - Equal: false, - }, - { - Name: "equal map int-typed", - Before: map[string]interface{}{"foo": 42}, - After: map[string]int{"foo": 42}, - Equal: true, - }, - { - Name: "unequal map int-typed", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]int{"foo": 42}, - Equal: false, - }, - { - Name: "equal map", - Before: map[string]interface{}{"foo": "bar", "42": 42}, - After: map[string]interface{}{"foo": "bar", "42": 42}, - Equal: true, - }, - { - Name: "unequal map", - Before: map[string]interface{}{"foo": "bar", "42": 42}, - After: map[string]interface{}{"foo": "bar", "42": 27}, - Equal: false, - }, - { - Name: "equal map empty string", - Before: map[string]interface{}{"foo": ""}, - After: map[string]interface{}{"foo": ""}, - Equal: true, - }, - { - Name: "unequal map replace empty string", - Before: map[string]interface{}{"foo": ""}, - After: map[string]interface{}{"foo": "bar"}, - Equal: false, - }, - { - Name: "unequal map replace with empty string", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]interface{}{"foo": ""}, - Equal: false, - }, - { - Name: "shorter map", - Before: map[string]interface{}{"foo": "bar", "42": 42}, - After: map[string]interface{}{"foo": "bar"}, - Equal: false, - }, - { - Name: "longer map", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]interface{}{"foo": "bar", "42": 42}, - Equal: false, - }, - { - Name: "different map", - Before: map[string]interface{}{"foo": "bar"}, - After: map[string]interface{}{"baz": "bar"}, - Equal: false, - }, - { - Name: "equal list string-typed", - Before: []interface{}{"foo", "bar"}, - After: []string{"foo", "bar"}, - Equal: true, - }, - { - Name: "unequal list string-typed", - Before: []interface{}{"foo", "bar"}, - After: []string{"foo", "baz"}, - Equal: false, - }, - { - Name: "equal list", - Before: []interface{}{"foo", 42}, - After: []interface{}{"foo", 42}, - Equal: true, - }, - { - Name: "unequal list", - Before: []interface{}{"foo", 42}, - After: []interface{}{"foo", 27}, - Equal: false, - }, - { - Name: "shorter list", - Before: []interface{}{"foo", 42}, - After: []interface{}{"foo"}, - Equal: false, - }, - { - Name: "longer list", - Before: []interface{}{"foo"}, - After: []interface{}{"foo", 42}, - Equal: false, - }, - } - - for _, c := range cases { - t.Run(c.Name, func(t *testing.T) { - var ne = "=" - if c.Equal { - ne = "!" - } - assert.Equal(t, c.Equal, optionValueEqual(c.Before, c.After), - fmt.Sprintf("%v %s= %v", c.Before, ne, c.After)) - }) - } -} - -func TestGetUpdateOperation(t *testing.T) { - var nilp *string - barExist := "bar" - bar := "bar" - baz := "baz" - existingData := map[string]interface{}{ - "foo": "bar", - "foop": &barExist, - "nil": nilp, - } - cases := []struct { - Name string - Field string - NewValue interface{} - ExpectedOp nodes.UpdateOp - ExpectedValue string - }{ - { - Name: "add value", - Field: "baz", - NewValue: "quux", - ExpectedOp: nodes.AddOp, - }, - { - Name: "add value pointer", - Field: "baz", - NewValue: &bar, - ExpectedValue: bar, - ExpectedOp: nodes.AddOp, - }, - { - Name: "add pointer value pointer", - Field: "nil", - NewValue: &bar, - ExpectedValue: bar, - ExpectedOp: nodes.AddOp, - }, - { - Name: "keep value", - Field: "foo", - NewValue: "bar", - }, - { - Name: "keep pointer value", - Field: "foop", - NewValue: "bar", - }, - { - Name: "keep value pointer", - Field: "foo", - NewValue: &bar, - }, - { - Name: "keep pointer value pointer", - Field: "foop", - NewValue: &bar, - }, - { - Name: "change value", - Field: "foo", - NewValue: "baz", - ExpectedOp: nodes.AddOp, - }, - { - Name: "change pointer value", - Field: "foop", - NewValue: "baz", - ExpectedOp: nodes.AddOp, - }, - { - Name: "change value pointer", - Field: "foo", - NewValue: &baz, - ExpectedValue: baz, - ExpectedOp: nodes.AddOp, - }, - { - Name: "change pointer value pointer", - Field: "foop", - NewValue: &baz, - ExpectedValue: baz, - ExpectedOp: nodes.AddOp, - }, - { - Name: "delete value", - Field: "foo", - NewValue: nil, - ExpectedOp: nodes.RemoveOp, - }, - { - Name: "delete value pointer", - Field: "foo", - NewValue: nilp, - ExpectedOp: nodes.RemoveOp, - }, - { - Name: "nonexistent value", - Field: "bar", - NewValue: nil, - }, - } - - for _, c := range cases { - t.Run(c.Name, func(t *testing.T) { - path := fmt.Sprintf("test/%s", c.Field) - updateOp := getUpdateOperation( - c.Field, existingData, - c.NewValue, - path, logr.Logger{}) - - switch c.ExpectedOp { - case nodes.AddOp, nodes.ReplaceOp: - assert.NotNil(t, updateOp) - ev := c.ExpectedValue - if ev == "" { - ev = c.NewValue.(string) - } - assert.Equal(t, c.ExpectedOp, updateOp.Op) - assert.Equal(t, ev, updateOp.Value) - assert.Equal(t, path, updateOp.Path) - case nodes.RemoveOp: - assert.NotNil(t, updateOp) - assert.Equal(t, c.ExpectedOp, updateOp.Op) - assert.Equal(t, path, updateOp.Path) - default: - assert.Nil(t, updateOp) - } - }) - } -} - -func TestTopLevelUpdateOpt(t *testing.T) { - u := updateOptsBuilder(logf.Log) - u.SetTopLevelOpt("foo", "baz", "bar") - ops := u.Updates - assert.Len(t, ops, 1) - op := ops[0].(nodes.UpdateOperation) - assert.Equal(t, nodes.AddOp, op.Op) - assert.Equal(t, "baz", op.Value) - assert.Equal(t, "/foo", op.Path) - - u = updateOptsBuilder(logf.Log) - u.SetTopLevelOpt("foo", "bar", "bar") - assert.Len(t, u.Updates, 0) -} - -func TestPropertiesUpdateOpts(t *testing.T) { - newValues := optionsData{ - "foo": "bar", - "baz": "quux", - } - node := nodes.Node{ - Properties: map[string]interface{}{ - "foo": "bar", - }, - } - - u := updateOptsBuilder(logf.Log) - u.SetPropertiesOpts(newValues, &node) - ops := u.Updates - assert.Len(t, ops, 1) - op := ops[0].(nodes.UpdateOperation) - assert.Equal(t, nodes.AddOp, op.Op) - assert.Equal(t, "quux", op.Value) - assert.Equal(t, "/properties/baz", op.Path) -} - -func TestInstanceInfoUpdateOpts(t *testing.T) { - newValues := optionsData{ - "foo": "bar", - "baz": "quux", - } - node := nodes.Node{ - InstanceInfo: map[string]interface{}{ - "foo": "bar", - }, - } - - u := updateOptsBuilder(logf.Log) - u.SetInstanceInfoOpts(newValues, &node) - ops := u.Updates - assert.Len(t, ops, 1) - op := ops[0].(nodes.UpdateOperation) - assert.Equal(t, nodes.AddOp, op.Op) - assert.Equal(t, "quux", op.Value) - assert.Equal(t, "/instance_info/baz", op.Path) -} - -func TestGetUpdateOptsForNodeWithRootHints(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHost() - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{} - - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - RootDeviceHints: host.Status.Provisioning.RootDeviceHints, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Map map[string]string // Expected roothdevicehint map - Value interface{} // the value being passed to ironic (or value associated with the key) - }{ - { - Path: "/properties/root_device", - Value: "userdefined_devicename", - Map: map[string]string{ - "name": "s== userd_devicename", - "hctl": "s== 1:2:3:4", - "model": " userd_model", - "vendor": " userd_vendor", - "serial": "s== userd_serial", - "size": ">= 40", - "wwn": "s== userd_wwn", - "wwn_with_extension": "s== userd_with_extension", - "wwn_vendor_extension": "s== userd_vendor_extension", - "rotational": "true", - }, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - if e.Map != nil { - assert.Equal(t, e.Map, update.Value, fmt.Sprintf("%s does not match", e.Path)) - } else { - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - } - }) - } -} - -func TestGetUpdateOptsForNodeVirtual(t *testing.T) { - host := metal3api.BareMetalHost{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myhost", - Namespace: "myns", - UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - Spec: metal3api.BareMetalHostSpec{ - BMC: metal3api.BMCDetails{ - Address: "test://test.bmc/", - }, - Image: &metal3api.Image{ - URL: "not-empty", - Checksum: "checksum", - ChecksumType: metal3api.MD5, - DiskFormat: pointer.StringPtr("raw"), - }, - Online: true, - HardwareProfile: "unknown", - }, - Status: metal3api.BareMetalHostStatus{ - HardwareProfile: "libvirt", - Provisioning: metal3api.ProvisionStatus{ - ID: "provisioning-id", - }, - }, - } - - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(errors.Wrap(err, "could not create provisioner")) - } - ironicNode := &nodes.Node{} - - hwProf, _ := profile.GetProfile("libvirt") - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - HardwareProfile: hwProf, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - }{ - { - Path: "/instance_info/image_source", - Value: "not-empty", - }, - { - Path: "/instance_info/image_os_hash_algo", - Value: "md5", - }, - { - Path: "/instance_info/image_os_hash_value", - Value: "checksum", - }, - { - Path: "/instance_info/image_disk_format", - Value: "raw", - }, - { - Path: "/instance_info/capabilities", - Value: map[string]string{}, - }, - { - Path: "/instance_uuid", - Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeDell(t *testing.T) { - host := metal3api.BareMetalHost{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myhost", - Namespace: "myns", - UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - Spec: metal3api.BareMetalHostSpec{ - BMC: metal3api.BMCDetails{ - Address: "test://test.bmc/", - }, - Image: &metal3api.Image{ - URL: "not-empty", - Checksum: "checksum", - ChecksumType: metal3api.MD5, - // DiskFormat not given to verify it is not added in instance_info - }, - Online: true, - }, - Status: metal3api.BareMetalHostStatus{ - HardwareProfile: "dell", - Provisioning: metal3api.ProvisionStatus{ - ID: "provisioning-id", - }, - }, - } - - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{} - - hwProf, _ := profile.GetProfile("dell") - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - HardwareProfile: hwProf, - CPUArchitecture: "x86_64", - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - }{ - { - Path: "/instance_info/image_source", - Value: "not-empty", - }, - { - Path: "/instance_info/image_os_hash_algo", - Value: "md5", - }, - { - Path: "/instance_info/image_os_hash_value", - Value: "checksum", - }, - { - Path: "/instance_uuid", - Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - { - Path: "/properties/cpu_arch", - Value: "x86_64", - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeLiveIso(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHostLiveIso() - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{} - - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/instance_info/boot_iso", - Value: "not-empty", - Op: nodes.AddOp, - }, - { - Path: "/instance_info/capabilities", - Value: map[string]string{}, - }, - { - Path: "/deploy_interface", - Value: "ramdisk", - Op: nodes.AddOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeImageToLiveIso(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHostLiveIso() - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{ - InstanceInfo: map[string]interface{}{ - "image_source": "oldimage", - "image_os_hash_value": "thechecksum", - "image_os_hash_algo": "md5", - }, - } - - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/instance_info/boot_iso", - Value: "not-empty", - Op: nodes.AddOp, - }, - { - Path: "/deploy_interface", - Value: "ramdisk", - Op: nodes.AddOp, - }, - { - Path: "/instance_info/image_source", - Op: nodes.RemoveOp, - }, - { - Path: "/instance_info/image_os_hash_algo", - Op: nodes.RemoveOp, - }, - { - Path: "/instance_info/image_os_hash_value", - Op: nodes.RemoveOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) - assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeLiveIsoToImage(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHost() - host.Spec.Image.URL = "newimage" - host.Spec.Image.Checksum = "thechecksum" - host.Spec.Image.ChecksumType = metal3api.MD5 - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{ - InstanceInfo: map[string]interface{}{ - "boot_iso": "oldimage", - }, - DeployInterface: "ramdisk", - } - - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/instance_info/boot_iso", - Op: nodes.RemoveOp, - }, - { - Path: "/deploy_interface", - Op: nodes.RemoveOp, - }, - { - Path: "/instance_info/image_source", - Value: "newimage", - Op: nodes.AddOp, - }, - { - Path: "/instance_info/image_os_hash_algo", - Value: "md5", - Op: nodes.AddOp, - }, - { - Path: "/instance_info/image_os_hash_value", - Value: "thechecksum", - Op: nodes.AddOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) - assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeCustomDeploy(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHostCustomDeploy(true) - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{} - - provData := provisioner.ProvisionData{ - Image: metal3api.Image{}, - BootMode: metal3api.DefaultBootMode, - CustomDeploy: host.Spec.CustomDeploy, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/instance_uuid", - Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - { - Path: "/deploy_interface", - Value: "custom-agent", - Op: nodes.AddOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeCustomDeployWithImage(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHostCustomDeploy(false) - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{} - - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.DefaultBootMode, - CustomDeploy: host.Spec.CustomDeploy, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/instance_info/image_source", - Value: "not-empty", - }, - { - Path: "/instance_uuid", - Value: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - { - Path: "/deploy_interface", - Value: "custom-agent", - Op: nodes.AddOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeImageToCustomDeploy(t *testing.T) { - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - host := makeHostCustomDeploy(false) - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(err) - } - ironicNode := &nodes.Node{ - InstanceInfo: map[string]interface{}{ - "image_source": "oldimage", - "image_os_hash_value": "thechecksum", - "image_os_hash_algo": "md5", - }, - } - - provData := provisioner.ProvisionData{ - Image: metal3api.Image{}, - BootMode: metal3api.DefaultBootMode, - CustomDeploy: host.Spec.CustomDeploy, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - Op nodes.UpdateOp // The operation add/replace/remove - }{ - { - Path: "/deploy_interface", - Value: "custom-agent", - Op: nodes.AddOp, - }, - { - Path: "/instance_info/image_source", - Op: nodes.RemoveOp, - }, - { - Path: "/instance_info/image_os_hash_algo", - Op: nodes.RemoveOp, - }, - { - Path: "/instance_info/image_os_hash_value", - Op: nodes.RemoveOp, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s value does not match", e.Path)) - assert.Equal(t, e.Op, update.Op, fmt.Sprintf("%s operation does not match", e.Path)) - }) - } -} - -func TestGetUpdateOptsForNodeSecureBoot(t *testing.T) { - host := metal3api.BareMetalHost{ - ObjectMeta: metav1.ObjectMeta{ - Name: "myhost", - Namespace: "myns", - UID: "27720611-e5d1-45d3-ba3a-222dcfaa4ca2", - }, - Spec: metal3api.BareMetalHostSpec{ - BMC: metal3api.BMCDetails{ - Address: "test://test.bmc/", - }, - Image: &metal3api.Image{ - URL: "not-empty", - Checksum: "checksum", - ChecksumType: metal3api.MD5, - DiskFormat: pointer.StringPtr("raw"), - }, - Online: true, - HardwareProfile: "unknown", - }, - Status: metal3api.BareMetalHostStatus{ - HardwareProfile: "libvirt", - Provisioning: metal3api.ProvisionStatus{ - ID: "provisioning-id", - }, - }, - } - - eventPublisher := func(reason, message string) {} - auth := clients.AuthConfig{Type: clients.NoAuth} - - prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, eventPublisher, "https://ironic.test", auth) - if err != nil { - t.Fatal(errors.Wrap(err, "could not create provisioner")) - } - ironicNode := &nodes.Node{} - - hwProf, _ := profile.GetProfile("libvirt") - provData := provisioner.ProvisionData{ - Image: *host.Spec.Image, - BootMode: metal3api.UEFISecureBoot, - HardwareProfile: hwProf, - } - patches := prov.getUpdateOptsForNode(ironicNode, provData).Updates - - t.Logf("patches: %v", patches) - - expected := []struct { - Path string // the node property path - Key string // if value is a map, the key we care about - Value interface{} // the value being passed to ironic (or value associated with the key) - }{ - { - Path: "/instance_info/image_source", - Value: "not-empty", - }, - { - Path: "/instance_info/capabilities", - Value: map[string]string{ - "secure_boot": "true", - }, - }, - } - - for _, e := range expected { - t.Run(e.Path, func(t *testing.T) { - t.Logf("expected: %v", e) - var update nodes.UpdateOperation - for _, patch := range patches { - update = patch.(nodes.UpdateOperation) - if update.Path == e.Path { - break - } - } - if update.Path != e.Path { - t.Errorf("did not find %q in updates", e.Path) - return - } - t.Logf("update: %v", update) - assert.Equal(t, e.Value, update.Value, fmt.Sprintf("%s does not match", e.Path)) - }) - } -} - -func TestSanitisedValue(t *testing.T) { - unchanged := []interface{}{ - "foo", - 42, - true, - []string{"bar", "baz"}, - map[string]string{"foo": "bar"}, - map[string][]string{"foo": {"bar", "baz"}}, - map[string]interface{}{"foo": []string{"bar", "baz"}, "bar": 42}, - } - - for _, u := range unchanged { - assert.Exactly(t, u, sanitisedValue(u)) - } - - unsafe := map[string]interface{}{ - "foo": "bar", - "password": "secret", - "ipmi_password": "secret", - } - safe := map[string]interface{}{ - "foo": "bar", - "password": "", - "ipmi_password": "", - } - assert.Exactly(t, safe, sanitisedValue(unsafe)) -}