diff --git a/controllers/metal3.io/host_config_data.go b/controllers/metal3.io/host_config_data.go index 584a87315e..630e03b8cd 100644 --- a/controllers/metal3.io/host_config_data.go +++ b/controllers/metal3.io/host_config_data.go @@ -4,17 +4,13 @@ import ( "context" "fmt" - "github.com/pkg/errors" - - metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" - "github.com/go-logr/logr" - + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" + + metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" ) // hostConfigData is an implementation of host configuration data interface. diff --git a/pkg/provisioner/fixture/fixture.go b/pkg/provisioner/fixture/fixture.go index e4feb2f46e..ae5979095a 100644 --- a/pkg/provisioner/fixture/fixture.go +++ b/pkg/provisioner/fixture/fixture.go @@ -15,6 +15,32 @@ var log = logf.Log.WithName("fixture") var deprovisionRequeueDelay = time.Second * 10 var provisionRequeueDelay = time.Second * 10 +type fixtureHostConfigData struct { + userData string + networkData string + metaData string +} + +func NewHostConfigData(userData string, networkData string, metaData string) provisioner.HostConfigData { + return &fixtureHostConfigData{ + userData: userData, + networkData: networkData, + metaData: metaData, + } +} + +func (cd *fixtureHostConfigData) UserData() (string, error) { + return cd.userData, nil +} + +func (cd *fixtureHostConfigData) NetworkData() (string, error) { + return cd.networkData, nil +} + +func (cd *fixtureHostConfigData) MetaData() (string, error) { + return cd.metaData, nil +} + // fixtureProvisioner implements the provisioning.fixtureProvisioner interface // and uses Ironic to manage the host. type fixtureProvisioner struct { diff --git a/pkg/provisioner/ironic/adopt_test.go b/pkg/provisioner/ironic/adopt_test.go new file mode 100644 index 0000000000..3363c181ea --- /dev/null +++ b/pkg/provisioner/ironic/adopt_test.go @@ -0,0 +1,114 @@ +package ironic + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/stretchr/testify/assert" + + "github.com/metal3-io/baremetal-operator/pkg/bmc" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver" +) + +func TestAdopt(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "node-in-enroll", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Enroll), + UUID: nodeUUID, + }), + + expectedDirty: false, + expectedError: true, + }, + { + name: "node-in-manageable", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Manageable), + UUID: nodeUUID, + }).WithNodeStatesProvision(nodeUUID), + + expectedDirty: true, + expectedRequestAfter: 10, + }, + { + name: "node-in-adopting", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Adopting), + UUID: nodeUUID, + }), + + expectedDirty: true, + expectedRequestAfter: 10, + }, + { + name: "node-in-verifying", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Verifying), + UUID: nodeUUID, + }), + + expectedDirty: true, + expectedRequestAfter: 10, + }, + { + name: "node-in-AdoptFail", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.AdoptFail), + UUID: nodeUUID, + }), + + expectedDirty: false, + expectedRequestAfter: 0, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, err := prov.Adopt() + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/pkg/provisioner/ironic/ironic.go b/pkg/provisioner/ironic/ironic.go index 4ed4c0d1c0..b971272eff 100644 --- a/pkg/provisioner/ironic/ironic.go +++ b/pkg/provisioner/ironic/ironic.go @@ -6,17 +6,14 @@ import ( "strings" "time" - "sigs.k8s.io/yaml" - + "github.com/go-logr/logr" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" "github.com/gophercloud/gophercloud/openstack/baremetal/v1/ports" "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" - "github.com/pkg/errors" - - "github.com/go-logr/logr" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/yaml" metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1" "github.com/metal3-io/baremetal-operator/pkg/bmc" diff --git a/pkg/provisioner/ironic/power_test.go b/pkg/provisioner/ironic/power_test.go new file mode 100644 index 0000000000..9b45d8e9de --- /dev/null +++ b/pkg/provisioner/ironic/power_test.go @@ -0,0 +1,214 @@ +package ironic + +import ( + "net/http" + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/stretchr/testify/assert" + + "github.com/metal3-io/baremetal-operator/pkg/bmc" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver" +) + +func TestPowerOn(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "node-already-power-on", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOn, + UUID: nodeUUID, + }), + }, + { + name: "waiting-for-target-power-on", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOff, + TargetPowerState: powerOn, + UUID: nodeUUID, + }), + expectedDirty: true, + expectedRequestAfter: 10, + }, + { + name: "power-on normal", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOff, + TargetPowerState: powerOff, + TargetProvisionState: "", + UUID: nodeUUID, + }).WithNodeStatesPower(nodeUUID, http.StatusAccepted), + expectedDirty: true, + }, + { + name: "power-on wait for Provisioning state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOff, + TargetPowerState: powerOff, + TargetProvisionState: string(nodes.TargetDeleted), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "power-on wait for locked host", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOff, + TargetPowerState: powerOff, + TargetProvisionState: "", + UUID: nodeUUID, + }).WithNodeStatesPower(nodeUUID, http.StatusConflict), + expectedRequestAfter: 10, + expectedDirty: true, + expectedError: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, err := prov.PowerOn() + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestPowerOff(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "node-already-power-off", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOff, + UUID: nodeUUID, + }), + }, + { + name: "waiting-for-target-power-off", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOn, + TargetPowerState: powerOff, + UUID: nodeUUID, + }), + expectedDirty: true, + expectedRequestAfter: 10, + }, + { + name: "power-off normal", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOn, + TargetPowerState: powerOn, + TargetProvisionState: "", + UUID: nodeUUID, + }).WithNodeStatesPower(nodeUUID, http.StatusAccepted), + expectedDirty: true, + }, + { + name: "power-off wait for Provisioning state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOn, + TargetPowerState: powerOn, + TargetProvisionState: string(nodes.TargetDeleted), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "power-off wait for locked host", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + PowerState: powerOn, + TargetPowerState: powerOn, + TargetProvisionState: "", + UUID: nodeUUID, + }).WithNodeStatesPower(nodeUUID, http.StatusConflict), + expectedRequestAfter: 10, + expectedDirty: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, err := prov.PowerOff() + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/pkg/provisioner/ironic/provision_test.go b/pkg/provisioner/ironic/provision_test.go new file mode 100644 index 0000000000..8aec8625a7 --- /dev/null +++ b/pkg/provisioner/ironic/provision_test.go @@ -0,0 +1,249 @@ +package ironic + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/stretchr/testify/assert" + + "github.com/metal3-io/baremetal-operator/pkg/bmc" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/fixture" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/clients" + "github.com/metal3-io/baremetal-operator/pkg/provisioner/ironic/testserver" +) + +func TestProvision(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "deployFail state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.DeployFail), + UUID: nodeUUID, + }).WithNodeValidate(nodeUUID).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 0, + expectedDirty: true, + }, + { + name: "manageable state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Manageable), + UUID: nodeUUID, + }).WithNodeValidate(nodeUUID).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 0, + expectedDirty: true, + }, + { + name: "available state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Available), + UUID: nodeUUID, + }).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "active state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Active), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: false, + }, + { + name: "other state: Cleaning", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Cleaning), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, err := prov.Provision(fixture.NewHostConfigData("testUserData", "test: NetworkData", "test: Meta")) + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestDeprovision(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + cases := []struct { + name string + ironic *testserver.IronicMock + expectedDirty bool + expectedError bool + expectedRequestAfter int + }{ + { + name: "error state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Error), + UUID: nodeUUID, + }).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 0, + expectedDirty: true, + }, + { + name: "available state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Available), + UUID: nodeUUID, + }).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "inspecting state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Inspecting), + UUID: nodeUUID, + }), + expectedRequestAfter: 15, + expectedDirty: true, + }, + { + name: "inspectWait state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.InspectWait), + UUID: nodeUUID, + }).WithNodeStatesProvision(nodeUUID), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "deleting state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Deleting), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "cleaning state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Cleaning), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + { + name: "cleanWait state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.CleanWait), + UUID: nodeUUID, + }), + expectedRequestAfter: 10, + expectedDirty: true, + }, + + { + name: "Manageable state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Manageable), + UUID: nodeUUID, + }), + expectedRequestAfter: 0, + expectedDirty: false, + }, + { + name: "Enroll state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Enroll), + UUID: nodeUUID, + }), + expectedRequestAfter: 0, + expectedDirty: false, + }, + { + name: "Verifying state", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + ProvisionState: string(nodes.Verifying), + UUID: nodeUUID, + }), + expectedRequestAfter: 0, + expectedDirty: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + inspector := testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }) + inspector.Start() + defer inspector.Stop() + + host := makeHost() + publisher := func(reason, message string) {} + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, err := prov.Deprovision() + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + if !tc.expectedError { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go index 2cfb982ec0..37d77bf6b5 100644 --- a/pkg/provisioner/ironic/testserver/ironic.go +++ b/pkg/provisioner/ironic/testserver/ironic.go @@ -134,3 +134,15 @@ func (m *IronicMock) CreateNodes() *IronicMock { }) return m } + +// WithNodeStatesPower configures the server with a valid response for /v1/nodes//states/power +func (m *IronicMock) WithNodeStatesPower(nodeUUID string, code int) *IronicMock { + m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/states/power", "{}", code) + return m +} + +// WithNodeValidate configures the server with a valid response for /v1/nodes//validate +func (m *IronicMock) WithNodeValidate(nodeUUID string) *IronicMock { + m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/validate", "{}", http.StatusOK) + return m +}