From 266b3df0b7c9ed1f453393a0f32bc816cadf9f4d Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 6 Jun 2025 13:53:53 -0700 Subject: [PATCH] Print data in SMD format --- cmd/pdu-collect.go | 50 +++++++++++++++++++++++++++++++--- pkg/jaws/pdu-crawler.go | 60 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index ecc9e68..216b48f 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -10,6 +10,44 @@ import ( "github.com/spf13/cobra" ) +func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { + smdRecords := make([]map[string]any, 0) + + rtsHostname := fmt.Sprintf("%s-rts:8083", inventory.Hostname) + pduBank := "B" + + for _, outlet := range inventory.Outlets { + smdID := fmt.Sprintf("%sp1v%s", inventory.Hostname, outlet.ID) + odataID := fmt.Sprintf("/redfish/v1/PowerEquipment/RackPDUs/%s/Outlets/%s", pduBank, outlet.ID) + redfishURL := fmt.Sprintf("%s%s", rtsHostname, odataID) + powerControlTarget := fmt.Sprintf("%s/Actions/Outlet.PowerControl", odataID) + + record := map[string]any{ + "ID": smdID, + "Type": "CabinetPDUPowerConnector", + "RedfishType": "Outlet", + "RedfishSubtype": "Cx", + "OdataID": odataID, + "RedfishEndpointID": inventory.Hostname, + "Enabled": true, + "RedfishEndpointFQDN": rtsHostname, + "RedfishURL": redfishURL, + "ComponentEndpointType": "ComponentEndpointOutlet", + "RedfishOutletInfo": map[string]any{ + "Name": outlet.Name, // Directly from inventory + "Actions": map[string]any{ + "#Outlet.PowerControl": map[string]any{ + "PowerState@Redfish.AllowableValues": []string{"On", "Off"}, + "target": powerControlTarget, + }, + }, + }, + } + smdRecords = append(smdRecords, record) + } + return smdRecords +} + var pduCollectCmd = &cobra.Command{ Use: "collect [hosts...]", Short: "Collect inventory from JAWS-based PDUs", @@ -25,7 +63,8 @@ var pduCollectCmd = &cobra.Command{ return } - collection := make([]*pdu.PDUInventory, 0) + allSmdRecords := make([]map[string]any, 0) + for _, host := range args { log.Info().Msgf("Collecting from PDU: %s", host) config := jaws.CrawlerConfig{ @@ -40,12 +79,15 @@ var pduCollectCmd = &cobra.Command{ log.Error().Err(err).Msgf("failed to crawl PDU %s", host) continue } - collection = append(collection, inventory) + + smdRecords := transformToSMDFormat(inventory) + + allSmdRecords = append(allSmdRecords, smdRecords...) } - output, err := json.MarshalIndent(collection, "", " ") + output, err := json.MarshalIndent(allSmdRecords, "", " ") if err != nil { - log.Error().Err(err).Msgf("failed to marshal PDU collection to JSON") + log.Error().Err(err).Msgf("failed to marshal SMD records to JSON") } fmt.Println(string(output)) }, diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go index a73c67c..ca4e27d 100644 --- a/pkg/jaws/pdu-crawler.go +++ b/pkg/jaws/pdu-crawler.go @@ -30,6 +30,45 @@ type JawsOutlet struct { ActivePower int `json:"active_power"` } +// JawsSystemInfo is the struct to unmarshal /jaws/config/info/system +type JawsSystemInfo struct { + Model string `json:"model"` + SerialNumber string `json:"serial_number"` + FirmwareVersion string `json:"firmware_version"` +} + +// getSystemInfo queries the PDU for overall system details. +func getSystemInfo(client *http.Client, config CrawlerConfig) (*JawsSystemInfo, error) { + targetURL := fmt.Sprintf("https://%s/jaws/config/info/system", config.URI) + req, err := http.NewRequest("GET", targetURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create system info request: %w", err) + } + req.SetBasicAuth(config.Username, config.Password) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute system info request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 status code from system info endpoint: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read system info response body: %w", err) + } + + var systemInfo JawsSystemInfo + if err := json.Unmarshal(body, &systemInfo); err != nil { + return nil, fmt.Errorf("failed to unmarshal system info: %w", err) + } + return &systemInfo, nil +} + +// CrawlPDU connects to a single JAWS PDU and collects its full inventory. func CrawlPDU(config CrawlerConfig) (*pdu.PDUInventory, error) { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: config.Insecure}, @@ -43,36 +82,45 @@ func CrawlPDU(config CrawlerConfig) (*pdu.PDUInventory, error) { Hostname: config.URI, } + systemInfo, err := getSystemInfo(client, config) + if err != nil { + log.Warn().Err(err).Msgf("could not retrieve system info for %s, proceeding without it", config.URI) + } else if systemInfo != nil { + inventory.Model = systemInfo.Model + inventory.SerialNumber = systemInfo.SerialNumber + inventory.FirmwareVersion = systemInfo.FirmwareVersion + log.Info().Msgf("successfully collected system info from %s", config.URI) + } + targetURL := fmt.Sprintf("https://%s/jaws/monitor/outlets", config.URI) req, err := http.NewRequest("GET", targetURL, nil) if err != nil { - log.Error().Err(err).Msg("failed to create new HTTP request") + log.Error().Err(err).Msg("failed to create new HTTP request for outlets") return nil, err } - req.SetBasicAuth(config.Username, config.Password) log.Debug().Msgf("querying JAWS endpoint: %s", targetURL) resp, err := client.Do(req) if err != nil { - log.Error().Err(err).Msgf("failed to execute request to JAWS endpoint %s", targetURL) + log.Error().Err(err).Msgf("failed to execute request to JAWS outlets endpoint %s", targetURL) return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - err := fmt.Errorf("received non-200 status code: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + err := fmt.Errorf("received non-200 status code from outlets endpoint: %d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) log.Error().Err(err).Str("url", targetURL).Msg("bad response from PDU") return nil, err } body, err := io.ReadAll(resp.Body) if err != nil { - log.Error().Err(err).Msg("failed to read response body") + log.Error().Err(err).Msg("failed to read outlets response body") return nil, err } - log.Debug().RawJSON("response_body", body).Msg("received response from JAWS") + log.Debug().RawJSON("response_body", body).Msg("received response from JAWS outlets") var rawOutlets []JawsOutlet if err := json.Unmarshal(body, &rawOutlets); err != nil {