From 2f526433d464d945ab224930475dc76f9588a7f7 Mon Sep 17 00:00:00 2001 From: Andrea Fasano Date: Thu, 24 Sep 2020 09:28:43 -0400 Subject: [PATCH] Initial InspectHardware unit tests --- .../ironic/inspecthardware_test.go | 179 ++++++++++++++++++ pkg/provisioner/ironic/ironic_test.go | 1 + .../ironic/testserver/inspector.go | 30 ++- pkg/provisioner/ironic/testserver/ironic.go | 19 +- pkg/provisioner/ironic/testserver/server.go | 19 ++ 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 pkg/provisioner/ironic/inspecthardware_test.go diff --git a/pkg/provisioner/ironic/inspecthardware_test.go b/pkg/provisioner/ironic/inspecthardware_test.go new file mode 100644 index 0000000000..e2155527a0 --- /dev/null +++ b/pkg/provisioner/ironic/inspecthardware_test.go @@ -0,0 +1,179 @@ +package ironic + +import ( + "net/http" + "testing" + "time" + + "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" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" + "github.com/stretchr/testify/assert" +) + +func TestInspectHardware(t *testing.T) { + + nodeUUID := "33ce8659-7400-4c68-9535-d10766f07a58" + + cases := []struct { + name string + ironic *testserver.IronicMock + inspector *testserver.InspectorMock + + expectedDirty bool + expectedRequestAfter int + expectedResultError string + expectedDetailsHost string + + expectedPublish string + expectedError string + }{ + { + name: "introspection-status-start-new-hardware-inspection", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + ProvisionState: "active", + }).WithNodeStatesProvision(nodeUUID), + inspector: testserver.NewInspector(t).Ready().WithIntrospectionFailed(nodeUUID, http.StatusNotFound), + + expectedDirty: true, + expectedRequestAfter: 10, + expectedPublish: "InspectionStarted Hardware inspection started", + }, + { + name: "introspection-data-failed", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + }), + inspector: testserver.NewInspector(t).Ready(). + WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: true, + }). + WithIntrospectionDataFailed(nodeUUID, http.StatusBadRequest), + + expectedError: "failed to retrieve hardware introspection data: Bad request with: \\[GET http://127.0.0.1:.*/v1/introspection/33ce8659-7400-4c68-9535-d10766f07a58/data\\], error message: An error\\\n", + }, + { + name: "introspection-status-failed-404-retry-on-wait", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + ProvisionState: "inspect wait", + }), + inspector: testserver.NewInspector(t).Ready().WithIntrospectionFailed(nodeUUID, http.StatusNotFound), + + expectedDirty: true, + expectedRequestAfter: 15, + }, + { + name: "introspection-status-failed-extraction", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + ProvisionState: "inspecting", + }), + inspector: testserver.NewInspector(t).Ready().WithIntrospectionFailed(nodeUUID, http.StatusBadRequest), + + expectedError: "failed to extract hardware inspection status: Bad request with: \\[GET http://127.0.0.1:.*/v1/introspection/33ce8659-7400-4c68-9535-d10766f07a58\\], error message: An error\\\n", + }, + { + name: "introspection-status-failed-404-retry", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + ProvisionState: "inspecting", + }), + inspector: testserver.NewInspector(t).Ready().WithIntrospectionFailed(nodeUUID, http.StatusNotFound), + + expectedDirty: true, + expectedRequestAfter: 15, + }, + { + name: "introspection-aborted", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + }), + inspector: testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: true, + Error: "Canceled by operator", + }), + + expectedResultError: "Canceled by operator", + }, + { + name: "inspection-in-progress", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + }), + inspector: testserver.NewInspector(t).Ready().WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: false, + }), + expectedDirty: true, + expectedRequestAfter: 15, + }, + { + name: "inspection-completed", + ironic: testserver.NewIronic(t).Ready().WithNode(nodes.Node{ + UUID: nodeUUID, + }), + inspector: testserver.NewInspector(t).Ready(). + WithIntrospection(nodeUUID, introspection.Introspection{ + Finished: true, + }). + WithIntrospectionData(nodeUUID, introspection.Data{ + Inventory: introspection.InventoryType{ + Hostname: "node-0", + }, + }), + + expectedDirty: false, + expectedDetailsHost: "node-0", + expectedPublish: "InspectionComplete Hardware inspection completed", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.ironic != nil { + tc.ironic.Start() + defer tc.ironic.Stop() + } + + if tc.inspector != nil { + tc.inspector.Start() + defer tc.inspector.Stop() + } + + host := makeHost() + publishedMsg := "" + publisher := func(reason, message string) { + publishedMsg = reason + " " + message + } + auth := clients.AuthConfig{Type: clients.NoAuth} + prov, err := newProvisionerWithSettings(host, bmc.Credentials{}, publisher, + tc.ironic.Endpoint(), auth, tc.inspector.Endpoint(), auth, + ) + if err != nil { + t.Fatalf("could not create provisioner: %s", err) + } + + prov.status.ID = nodeUUID + result, details, err := prov.InspectHardware() + + assert.Equal(t, tc.expectedDirty, result.Dirty) + assert.Equal(t, time.Second*time.Duration(tc.expectedRequestAfter), result.RequeueAfter) + assert.Equal(t, tc.expectedResultError, result.ErrorMessage) + + if details != nil { + assert.Equal(t, tc.expectedDetailsHost, details.Hostname) + } + assert.Equal(t, tc.expectedPublish, publishedMsg) + if tc.expectedError == "" { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.Regexp(t, tc.expectedError, err.Error()) + } + }) + } +} diff --git a/pkg/provisioner/ironic/ironic_test.go b/pkg/provisioner/ironic/ironic_test.go index f7af6dba58..b553a0d3be 100644 --- a/pkg/provisioner/ironic/ironic_test.go +++ b/pkg/provisioner/ironic/ironic_test.go @@ -56,6 +56,7 @@ func makeHost() *metal3v1alpha1.BareMetalHost { WWNVendorExtension: "userd_vendor_extension", Rotational: &rotational, }, + BootMode: metal3v1alpha1.UEFI, }, HardwareProfile: "libvirt", }, diff --git a/pkg/provisioner/ironic/testserver/inspector.go b/pkg/provisioner/ironic/testserver/inspector.go index 51d87b78cc..4762912a69 100644 --- a/pkg/provisioner/ironic/testserver/inspector.go +++ b/pkg/provisioner/ironic/testserver/inspector.go @@ -1,6 +1,10 @@ package testserver -import "testing" +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/baremetalintrospection/v1/introspection" +) // InspectorMock is a test server that implements Ironic Inspector's semantics type InspectorMock struct { @@ -33,3 +37,27 @@ func (m *InspectorMock) NotReady(errorCode int) *InspectorMock { m.ErrorResponse("/v1", errorCode) return m } + +// WithIntrospection configures the server with a valid response for /v1/introspection/ +func (m *InspectorMock) WithIntrospection(nodeUUID string, status introspection.Introspection) *InspectorMock { + m.ResponseJSON("/v1/introspection/"+nodeUUID, status) + return m +} + +// WithIntrospectionFailed configures the server with an error response for /v1/introspection/ +func (m *InspectorMock) WithIntrospectionFailed(nodeUUID string, errorCode int) *InspectorMock { + m.ErrorResponse("/v1/introspection/"+nodeUUID, errorCode) + return m +} + +// WithIntrospectionData configures the server with a valid response for /v1/introspection//data +func (m *InspectorMock) WithIntrospectionData(nodeUUID string, data introspection.Data) *InspectorMock { + m.ResponseJSON("/v1/introspection/"+nodeUUID+"/data", data) + return m +} + +// WithIntrospectionDataFailed configures the server with an error response for /v1/introspection//data +func (m *InspectorMock) WithIntrospectionDataFailed(nodeUUID string, errorCode int) *InspectorMock { + m.ErrorResponse("/v1/introspection/"+nodeUUID+"/data", errorCode) + return m +} diff --git a/pkg/provisioner/ironic/testserver/ironic.go b/pkg/provisioner/ironic/testserver/ironic.go index a01dc5ba3b..9974a6b722 100644 --- a/pkg/provisioner/ironic/testserver/ironic.go +++ b/pkg/provisioner/ironic/testserver/ironic.go @@ -1,6 +1,11 @@ package testserver -import "testing" +import ( + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" +) // IronicMock is a test server that implements Ironic's semantics type IronicMock struct { @@ -58,3 +63,15 @@ func (m *IronicMock) WithDrivers() *IronicMock { `) return m } + +// WithNode configures the server with a valid response for /v1/nodes +func (m *IronicMock) WithNode(node nodes.Node) *IronicMock { + m.ResponseJSON("/v1/nodes/"+node.UUID, node) + return m +} + +// WithNodeStatesProvision configures the server with a valid response for /v1/nodes//states/provision +func (m *IronicMock) WithNodeStatesProvision(nodeUUID string) *IronicMock { + m.ResponseWithCode("/v1/nodes/"+nodeUUID+"/states/provision", "{}", http.StatusAccepted) + return m +} diff --git a/pkg/provisioner/ironic/testserver/server.go b/pkg/provisioner/ironic/testserver/server.go index dc0d8da982..53e7be6b8d 100644 --- a/pkg/provisioner/ironic/testserver/server.go +++ b/pkg/provisioner/ironic/testserver/server.go @@ -1,6 +1,7 @@ package testserver import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -72,15 +73,33 @@ func (m *MockServer) NotFound(pattern string) *MockServer { // Response attaches a handler function that returns the given payload // from requests to the URL pattern func (m *MockServer) Response(pattern string, payload string) *MockServer { + return m.ResponseWithCode(pattern, payload, http.StatusOK) +} + +// ResponseWithCode attaches a handler function that returns the given payload +// from requests to the URL pattern along with the specified code +func (m *MockServer) ResponseWithCode(pattern string, payload string, code int) *MockServer { m.t.Logf("%s: adding response handler for %s", m.name, pattern) m.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { m.logRequest(r, payload) w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) fmt.Fprint(w, payload) }) return m } +// ResponseJSON marshals the JSON object as payload returned by the response +// handler +func (m *MockServer) ResponseJSON(pattern string, payload interface{}) *MockServer { + content, err := json.Marshal(payload) + if err != nil { + m.t.Error(err) + } + m.Response(pattern, string(content)) + return m +} + // ErrorResponse attaches a handler function that returns the given // error code from requests to the URL pattern func (m *MockServer) ErrorResponse(pattern string, errorCode int) *MockServer {