From 0abe2dd5a8266da2684db8ea74733a411bea0b94 Mon Sep 17 00:00:00 2001 From: Niv Gal Waizer Date: Wed, 6 Jul 2022 22:39:39 +0300 Subject: [PATCH] SubmitTestEvent is diffrent for some oem vendors and zt event subscription --- oem/dell/dell.go | 53 +++++++++++ oem/dell/test/dell_test.go | 167 +++++++++++++++++++++++++++++++++ oem/hpe/events/events.go | 60 ++++++++++++ oem/hpe/hpe.go | 60 ++++++------ oem/zt/test/zt_test.go | 186 +++++++++++++++++++++++++++++++++++++ oem/zt/zt.go | 95 +++++++++++++++++++ 6 files changed, 591 insertions(+), 30 deletions(-) create mode 100644 oem/dell/dell.go create mode 100644 oem/dell/test/dell_test.go create mode 100644 oem/hpe/events/events.go create mode 100644 oem/zt/test/zt_test.go create mode 100644 oem/zt/zt.go diff --git a/oem/dell/dell.go b/oem/dell/dell.go new file mode 100644 index 00000000..4eb57d78 --- /dev/null +++ b/oem/dell/dell.go @@ -0,0 +1,53 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package dell + +import ( + "fmt" + "net/http" + + "github.com/stmcginnis/gofish/common" +) + +const eventContext string = "root" + +var SubmitTestEventTarget = "/redfish/v1/EventService/Actions/EventService.SendTestEvent" + +type ( + PayloadType struct { + Destination string `json:"Destination"` + EventTypes string `json:"EventTypes"` + Context string `json:"Context"` + Protocol string `json:"Protocol"` + MessageID string `json:"MessageId"` + } +) + +// SubmitTestEvent sends event according to msgId and returns error. +func SubmitTestEvent(client common.Client, messageID, eType, protocol string) error { + payload := PayloadType{ + Destination: SubmitTestEventTarget, + EventTypes: eType, + Context: eventContext, + Protocol: protocol, + MessageID: messageID, + } + resp, err := client.Post(SubmitTestEventTarget, payload) + + if err != nil { + return fmt.Errorf("failed to post submitTestEvent due to: %w", err) + } + defer resp.Body.Close() + + valid := map[int]bool{ + http.StatusNoContent: true, + http.StatusCreated: true} + + if !valid[resp.StatusCode] { + return fmt.Errorf("on send event received response: %v due to: %s", resp.StatusCode, resp.Body) + } + + return nil +} diff --git a/oem/dell/test/dell_test.go b/oem/dell/test/dell_test.go new file mode 100644 index 00000000..104f8f23 --- /dev/null +++ b/oem/dell/test/dell_test.go @@ -0,0 +1,167 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package test + +import ( + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/oem/dell" + "github.com/stmcginnis/gofish/redfish" +) + +const serviceRootBody = `{ + "@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_8_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Description": "Root Service", + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Fabrics": { + "@odata.id": "/redfish/v1/Fabrics" + }, + "Id": "RootService", + "JobService": { + "@odata.id": "/redfish/v1/JobService" + }, + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "Oem": { + "Dell": { + "@odata.context": "/redfish/v1/$metadata#DellServiceRoot.DellServiceRoot", + "@odata.type": "#DellServiceRoot.v1_0_0.DellServiceRoot", + "IsBranded": 0, + "ManagerMACAddress": "00:00:00:00:00:00", + "ServiceTag": "0000000" + } + }, + "Product": "Integrated Dell Remote Access Controller", + "ProtocolFeaturesSupported": { + "DeepOperations": { + "DeepPATCH": false, + "DeepPOST": false + }, + "ExcerptQuery": false, + "ExpandQuery": { + "ExpandAll": true, + "Levels": true, + "Links": true, + "MaxLevels": 1, + "NoLinks": true + }, + "FilterQuery": true, + "OnlyMemberQuery": true, + "SelectQuery": true + }, + "RedfishVersion": "1.11.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + }, + "Vendor": "Dell" +}` + +func TestDellSubmitTestEvent(t *testing.T) { + const redfishBaseURL = "/redfish/v1/" + var ( + c common.Client + err error + requestCounter int // this counter is used to verify that the received requests, are in the expected order + ) + + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet && + req.URL.String() == redfishBaseURL && + requestCounter == 0 { // ServiceRoot + log.Printf("Mock received login request") + contentType := req.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("gofish connect sent wrong header. Content-Type:"+ + " is %v and not expected application/json", contentType) + } + + requestCounter++ + + // Send response to be tested + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", "application/json") + + rw.Write([]byte(serviceRootBody)) // nolint:errcheck + } else if req.Method == http.MethodPost && // SubmitTestEvent + req.URL.String() == dell.SubmitTestEventTarget && + requestCounter == 1 { + log.Printf("Mock got SubmitTestEvent POST") + + err := json.NewDecoder(req.Body).Decode(&dell.PayloadType{}) + if err != nil { + t.Errorf("error in SubmitTestEvent payload for Dell due to: %v", err) + } + + requestCounter++ + + rw.WriteHeader(http.StatusCreated) + } else { + t.Errorf("mock got unexpected %v request to path %v while request counter is %v", + req.Method, req.URL.String(), requestCounter) + } + })) + // Close the server when test finishes + defer server.Close() + + c, err = gofish.Connect(gofish.ClientConfig{Endpoint: server.URL, HTTPClient: server.Client()}) + + if err != nil { + t.Errorf("failed to establish client to mock http server due to: %v", err) + } + + err = dell.SubmitTestEvent( + c, + "AMP0300", + "Alert", + string(redfish.RedfishEventDestinationProtocol)) + if err != nil { + log.Printf("Got error %v", err) + } +} diff --git a/oem/hpe/events/events.go b/oem/hpe/events/events.go new file mode 100644 index 00000000..95d81175 --- /dev/null +++ b/oem/hpe/events/events.go @@ -0,0 +1,60 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package events + +import ( + "fmt" + "net/http" + "time" + + "github.com/stmcginnis/gofish/common" +) + +var ( + submitTestEventTarget = "/redfish/v1/EventService/Actions/EventService.SendTestEvent" +) + +type hpePayloadType struct { + EventID string `json:"EventID"` + EventTimestamp string `json:"EventTimestamp"` + EventType string `json:"EventType"` + Message string + MessageArgs []string + MessageID string `json:"MessageId"` + OriginOfCondition string + Severity string +} + +// SubmitTestEvent sends event according to msgId and returns error +// more info https://hewlettpackard.github.io/iLOAmpPack-Redfish-API-Docs/#submitting-a-test-event +func SubmitTestEvent(client common.Client, msgID string) error { + payload := hpePayloadType{ + EventID: "TestEventId", + EventTimestamp: time.Now().Format(time.RFC3339), // "2019-07-29T15:13:49Z", + EventType: "Alert", // redfish.SupportedEventTypes["Alert"], + Message: "Test Event", + MessageArgs: []string{"NoAMS", "Busy", "Cached"}, + MessageID: msgID, + OriginOfCondition: "/redfish/v1/Systems/1/", + Severity: "OK", + } + resp, err := client.Post(submitTestEventTarget, payload) + + if err != nil { + return fmt.Errorf("failed to send submitTestEvent due to: %w", err) + } + defer resp.Body.Close() + + valid := map[int]bool{ + http.StatusOK: true, + http.StatusNoContent: true, + http.StatusCreated: true} + + if !valid[resp.StatusCode] { + return fmt.Errorf("on send event received response: %v due to: %s", resp.StatusCode, resp.Body) + } + + return nil +} diff --git a/oem/hpe/hpe.go b/oem/hpe/hpe.go index 8dfaab19..de0ce5ee 100644 --- a/oem/hpe/hpe.go +++ b/oem/hpe/hpe.go @@ -10,6 +10,36 @@ import ( "github.com/stmcginnis/gofish/redfish" ) +type Fan struct { + redfish.Fan + Oem FanOem +} + +type FanOem struct { + Hpe struct { + OdataContext string `json:"@odata.context"` + OdataType string `json:"@odata.type"` + Location string `json:"Location"` + Redundant bool `json:"Redundant"` + HotPluggable bool `json:"HotPluggable"` + } `json:"Hpe"` +} + +type Thermal struct { + redfish.Thermal + Fans []Fan + Oem ThermalOem +} + +type ThermalOem struct { + Hpe struct { + OdataContext string `json:"@odata.context"` + OdataType string `json:"@odata.type"` + ThermalConfiguration string `json:"ThermalConfiguration"` + FanPercentMinimum int `json:"FanPercentMinimum"` + } `json:"Hpe"` +} + func FromThermal(thermal *redfish.Thermal) (Thermal, error) { oem := ThermalOem{} @@ -43,33 +73,3 @@ func FromFan(fan *redfish.Fan) (Fan, error) { Oem: oem, }, nil } - -type Fan struct { - redfish.Fan - Oem FanOem -} - -type FanOem struct { - Hpe struct { - OdataContext string `json:"@odata.context"` - OdataType string `json:"@odata.type"` - Location string `json:"Location"` - Redundant bool `json:"Redundant"` - HotPluggable bool `json:"HotPluggable"` - } `json:"Hpe"` -} - -type Thermal struct { - redfish.Thermal - Fans []Fan - Oem ThermalOem -} - -type ThermalOem struct { - Hpe struct { - OdataContext string `json:"@odata.context"` - OdataType string `json:"@odata.type"` - ThermalConfiguration string `json:"ThermalConfiguration"` - FanPercentMinimum int `json:"FanPercentMinimum"` - } `json:"Hpe"` -} diff --git a/oem/zt/test/zt_test.go b/oem/zt/test/zt_test.go new file mode 100644 index 00000000..681c744b --- /dev/null +++ b/oem/zt/test/zt_test.go @@ -0,0 +1,186 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package test + +import ( + "log" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/oem/zt" + "github.com/stmcginnis/gofish/redfish" +) + +const ztSubscriptionURL = "/redfish/v1/EventService/Subscriptions" + +const serviceRootBody = `{ +"@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot", +"@odata.etag": "\"0000000000\"", +"@odata.id": "/redfish/v1/", +"@odata.type": "#ServiceRoot.v1_5_2.ServiceRoot", +"AccountService": { +"@odata.id": "/redfish/v1/AccountService" +}, +"CertificateService": { +"@odata.id": "/redfish/v1/CertificateService" +}, +"Chassis": { +"@odata.id": "/redfish/v1/Chassis" +}, +"CompositionService": { +"@odata.id": "/redfish/v1/CompositionService" +}, +"Description": "The service root for all Redfish requests on this host", +"EventService": { +"@odata.id": "/redfish/v1/EventService" +}, +"Id": "RootService", +"JsonSchemas": { +"@odata.id": "/redfish/v1/JsonSchemas" +}, +"Links": { +"Sessions": { +"@odata.id": "/redfish/v1/SessionService/Sessions" +} +}, +"Managers": { +"@odata.id": "/redfish/v1/Managers" +}, +"Name": "Root Service", +"Oem": { +"Ami": { +"@odata.type": "#AMIServiceRoot.v1_0_0.AMIServiceRoot", +"InventoryDataStatus": { +"@odata.id": "/redfish/v1/Oem/Ami/InventoryData/Status" +}, +"RtpVersion": "1.8.a", +"configurations": { +"@odata.id": "/redfish/v1/configurations" +} +} +}, +"Product": "AMI Redfish Server", +"ProtocolFeaturesSupported": { +"ExcerptQuery": true, +"ExpandQuery": { +"ExpandAll": true, +"Levels": true, +"Links": true, +"MaxLevels": 5, +"NoLinks": true +}, +"FilterQuery": true, +"OnlyMemberQuery": true, +"SelectQuery": true +}, +"RedfishVersion": "1.8.0", +"Registries": { +"@odata.id": "/redfish/v1/Registries" +}, +"SessionService": { +"@odata.id": "/redfish/v1/SessionService" +}, +"Systems": { +"@odata.id": "/redfish/v1/Systems" +}, +"Tasks": { +"@odata.id": "/redfish/v1/TaskService" +}, +"TelemetryService": { +"@odata.id": "/redfish/v1/TelemetryService" +}, +"UUID": "00000000-0000-0000-0000-000000000000", +"UpdateService": { +"@odata.id": "/redfish/v1/UpdateService" +}, +"Vendor": "AMI" +}` + +const subscribeResponseBody = `{ + "@odata.context": "/redfish/v1/$metadata#EventDestination.EventDestination", + "@odata.etag": "\"0000000000\"", + "@odata.id": "/redfish/v1/EventService/Subscriptions", + "@odata.type": "#EventDestination.v1_6_0.EventDestination", + "Context": "root", + "DeliveryRetryPolicy": "TerminateAfterRetries", + "Description": "Event Subscription", + "Destination": "https://events.receiver/events/", + "EventFormatType": "Event", + "Id": 1, + "Name": "Subscription 1", + "Protocol": "Redfish", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "SubordinateResources": false, + "SubscriptionType": "RedfishEvent" +}` + +func TestSubscribeZT(t *testing.T) { + const redfishBaseURL = "/redfish/v1/" + + var ( + c common.Client + requestCounter int // this counter is used to verify that the received requests, are in the expected order + ) + + // Start a local HTTP server + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodGet && + req.URL.String() == redfishBaseURL && + requestCounter == 0 { // ServiceRoot + log.Printf("Mock received login request") + contentType := req.Header.Get("Content-Type") + if contentType != "application/json" { + t.Errorf("gofish connect sent wrong header. Content-Type:"+ + " is %v and not expected application/json", contentType) + } + + requestCounter++ + // Send response to be tested + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", "application/json") + + rw.Write([]byte(serviceRootBody)) // nolint:errcheck + } else if req.Method == http.MethodPost && // Subscribe + req.URL.String() == ztSubscriptionURL && + requestCounter == 1 { + log.Printf("Mock got suscription POST") + + requestCounter++ + + rw.Write([]byte(subscribeResponseBody)) // nolint:errcheck + } else { + t.Errorf("mock got unexpected %v request to path %v while request counter is %v", + req.Method, req.URL.String(), requestCounter) + } + })) + // Close the server when test finishes + defer server.Close() + + c, err := gofish.Connect(gofish.ClientConfig{Endpoint: server.URL, HTTPClient: server.Client()}) + if err != nil { + t.Errorf("failed to establish redfish session due to: %v", err) + } + + url, err := zt.Subscribe(c, + ztSubscriptionURL, + "https://events.receiver/events/", + string(redfish.RedfishEventDestinationProtocol), + "root") + if err != nil { + t.Errorf("failed to Subscribe() due to: %v", err) + } + + expectedSubURL := "/redfish/v1/EventService/Subscriptions/1" + if url != expectedSubURL { + t.Errorf("expected subscription url: %v got %v", expectedSubURL, url) + } +} diff --git a/oem/zt/zt.go b/oem/zt/zt.go new file mode 100644 index 00000000..c0c37fef --- /dev/null +++ b/oem/zt/zt.go @@ -0,0 +1,95 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package zt + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/stmcginnis/gofish/common" +) + +var ( + SubmitTestEventTarget = "/redfish/v1/EventService/Actions/EventService.SendTestEvent" +) + +type SubscriptionZtRequestType struct { + Destination string `json:"Destination"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + Protocol string `json:"Protocol,omitempty"` + Context string `json:"Context,omitempty"` +} + +// subscribeResponseType zt uses a unique subscription response. +type subscribeZtResponseType struct { + ODataContext string `json:"@odata.context"` + ODataEtag string `json:"@odata.etag"` + ODataID string `json:"@odata.id"` + ODataType string `json:"@odata.type"` + Context string `json:"Context"` + DeliveryRetryPolicy string `json:"DeliveryRetryPolicy"` + Description string `json:"Description"` + Destination string `json:"Destination"` + EventFormatType string `json:"EventFormatType"` + ID int `json:"ID"` + Name string `json:"Name"` + Protocol string `json:"Protocol"` + common.Status `json:"Status"` + SubordinateResources bool `json:"SubordinateResources"` + SubscriptionType string `json:"SubscriptionType"` +} + +type eventPayload struct { + MessageID string `json:"MessageId"` +} + +func getSubscriptionURL(ztSubscribeResponse *subscribeZtResponseType) string { + return fmt.Sprintf("%s/%v", ztSubscribeResponse.ODataID, ztSubscribeResponse.ID) +} + +func Subscribe(c common.Client, subscriptionURL, eventsReceiverURL, protocol, eventContext string) (string, error) { + z := &SubscriptionZtRequestType{ + Destination: eventsReceiverURL, + Protocol: protocol, + Context: eventContext, + } + resp, err := c.Post(subscriptionURL, z) + + if err != nil { + return "", fmt.Errorf("failed to POST subscribe request to redfish due to %w", err) + } + defer resp.Body.Close() + + var ztSubscribeResponse subscribeZtResponseType + err = json.NewDecoder(resp.Body).Decode(&ztSubscribeResponse) + if err != nil { + return "", fmt.Errorf("failed to read response body from subscription request due to: %w", err) + } + + subscriptionLink := getSubscriptionURL(&ztSubscribeResponse) + return subscriptionLink, nil +} + +// SubmitTestEvent sends event according to msgId and returns error. +func SubmitTestEvent(client common.Client, msgID string) error { + p := eventPayload{ + MessageID: msgID, + } + resp, err := client.Post(SubmitTestEventTarget, p) + + if err != nil { + return fmt.Errorf("failed to send submitTestEvent in SubmitTestEvent() due to: %w", err) + } + defer resp.Body.Close() + + valid := map[int]bool{http.StatusAccepted: true} + + if !valid[resp.StatusCode] { + return fmt.Errorf("on send event received response: %v due to: %s", resp.StatusCode, resp.Body) + } + + return nil +}