From 4d965403f36d6ca80dab6376cebe1b5d3eb14c46 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 | 83 +++++++++++++++++++++++++++++ oem/dell/test/serviceRoot.json | 1 + oem/hpe/events/events.go | 60 +++++++++++++++++++++ oem/hpe/hpe.go | 60 ++++++++++----------- oem/zt/test/SubResp.json | 1 + oem/zt/test/serviceRoot.json | 1 + oem/zt/test/zt_test.go | 84 ++++++++++++++++++++++++++++++ oem/zt/zt.go | 95 ++++++++++++++++++++++++++++++++++ redfish/eventdestination.go | 8 ++- 10 files changed, 414 insertions(+), 32 deletions(-) create mode 100644 oem/dell/dell.go create mode 100644 oem/dell/test/dell_test.go create mode 100755 oem/dell/test/serviceRoot.json create mode 100644 oem/hpe/events/events.go create mode 100644 oem/zt/test/SubResp.json create mode 100644 oem/zt/test/serviceRoot.json 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..313bc3a4 --- /dev/null +++ b/oem/dell/test/dell_test.go @@ -0,0 +1,83 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package test + +import ( + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/oem/dell" + "github.com/stmcginnis/gofish/redfish" +) + +func TestSubscribeZT(t *testing.T) { + var ( + c common.Client + err error + ) + + mydir, err := os.Getwd() + if err != nil { + t.Errorf("error getting current directory: %v", err) + } + + // 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() == "/redfish/v1/" { // 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) + } + + // Send response to be tested + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", "application/json") + + filename := mydir + "/serviceRoot.json" + serviceRootData, err := os.ReadFile(filename) + if err != nil { + t.Errorf("failed to open file at %v due to: %v", filename, err) + } + + rw.Write(serviceRootData) // nolint:errcheck + } else if req.Method == http.MethodPost && // SubmitTestEvent + req.URL.String() == dell.SubmitTestEventTarget { + 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) + } + rw.WriteHeader(http.StatusCreated) + } else { + t.Errorf("mock got unexpected %v request to path %v", req.Method, req.URL.String()) + } + })) + // 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/dell/test/serviceRoot.json b/oem/dell/test/serviceRoot.json new file mode 100755 index 00000000..fa5ed13d --- /dev/null +++ b/oem/dell/test/serviceRoot.json @@ -0,0 +1 @@ +{"@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":"b0:7b:25:e3:54:f8","ServiceTag":"BB205G3"}},"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"} \ No newline at end of file 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/SubResp.json b/oem/zt/test/SubResp.json new file mode 100644 index 00000000..1aee995b --- /dev/null +++ b/oem/zt/test/SubResp.json @@ -0,0 +1 @@ +{"@odata.context":"/redfish/v1/$metadata#EventDestination.EventDestination","@odata.etag":"\"1657132569\"","@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"} \ No newline at end of file diff --git a/oem/zt/test/serviceRoot.json b/oem/zt/test/serviceRoot.json new file mode 100644 index 00000000..d3ad927e --- /dev/null +++ b/oem/zt/test/serviceRoot.json @@ -0,0 +1 @@ +{"@odata.context":"/redfish/v1/$metadata#ServiceRoot.ServiceRoot","@odata.etag":"\"1657118733\"","@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":"5a544443-6f10-0020-6265-d42066696c6c","UpdateService":{"@odata.id":"/redfish/v1/UpdateService"},"Vendor":"AMI"} \ No newline at end of file diff --git a/oem/zt/test/zt_test.go b/oem/zt/test/zt_test.go new file mode 100644 index 00000000..ccc345e0 --- /dev/null +++ b/oem/zt/test/zt_test.go @@ -0,0 +1,84 @@ +// +// SPDX-License-Identifier: BSD-3-Clause +// + +package test + +import ( + "github.com/stmcginnis/gofish" + "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/oem/zt" + "github.com/stmcginnis/gofish/redfish" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestSubscribeZT(t *testing.T) { + var ( + c common.Client + err error + ztSubscriptionURL = "/redfish/v1/EventService/Subscriptions" + ) + + mydir, err := os.Getwd() + if err != nil { + t.Errorf("error getting current directory: %v", err) + } + // 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() == "/redfish/v1/" { // 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) + } + + // Send response to be tested + rw.WriteHeader(http.StatusOK) + rw.Header().Set("Content-Type", "application/json") + + filename := mydir + "/serviceRoot.json" + serviceRootData, err := os.ReadFile(filename) + if err != nil { + t.Errorf("failed to open file at %v due to: %v", filename, err) + } + rw.Write(serviceRootData) // nolint:errcheck + } else if req.Method == http.MethodPost && // Subscribe + req.URL.String() == ztSubscriptionURL { + log.Printf("Mock got suscription POST") + + filename := mydir + "/SubResp.json" + subRespData, err := os.ReadFile(filename) + if err != nil { + t.Errorf("failed to open file at %v due to: %v", filename, err) + } + rw.Write(subRespData) // nolint:errcheck + } + t.Errorf("mock got unexpected %v request to path %v", req.Method, req.URL.String()) + })) + // 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 +} diff --git a/redfish/eventdestination.go b/redfish/eventdestination.go index 9d4e8b9a..cc69fc5a 100644 --- a/redfish/eventdestination.go +++ b/redfish/eventdestination.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/stmcginnis/gofish/common" + "github.com/stmcginnis/gofish/oem/zt" ) // DeliveryRetryPolicy is the retry policy for delivery failure. @@ -349,6 +350,10 @@ func CreateEventDestination( context string, oem interface{}, ) (string, error) { + // ZT systems uses a different payload for this action + if oem == "ZT" { + return zt.Subscribe(c, uri, destination, string(protocol), context) + } // validate input parameters err := validateCreateEventDestinationParams( uri, @@ -377,8 +382,8 @@ func CreateEventDestination( if oem != nil { s.Oem = oem } - resp, err := c.Post(uri, s) + if err != nil { return "", err } @@ -389,7 +394,6 @@ func CreateEventDestination( if urlParser, err := url.ParseRequestURI(subscriptionLink); err == nil { subscriptionLink = urlParser.RequestURI() } - return subscriptionLink, nil }