From 7aeb3aa6c56919c25a5a36aae84077da2de8ee56 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 5 Jun 2025 15:30:15 -0700 Subject: [PATCH 01/32] Add some initial files --- cmd/pdu-collect.go | 58 ++++++++++++++++++++++++++++++++++++++++ cmd/pdu.go | 15 +++++++++++ pkg/jaws/pdu-crawler.go | 52 +++++++++++++++++++++++++++++++++++ pkg/pdu/README.md | 1 + pkg/pdu/pdu-inventory.go | 15 +++++++++++ 5 files changed, 141 insertions(+) create mode 100644 cmd/pdu-collect.go create mode 100644 cmd/pdu.go create mode 100644 pkg/jaws/pdu-crawler.go create mode 100644 pkg/pdu/README.md create mode 100644 pkg/pdu/pdu-inventory.go diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go new file mode 100644 index 0000000..c8e0590 --- /dev/null +++ b/cmd/pdu-collect.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/OpenCHAMI/magellan/pkg/jaws" + "github.com/OpenCHAMI/magellan/pkg/pdu" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var pduCollectCmd = &cobra.Command{ + Use: "collect [hosts...]", + Short: "Collect inventory from JAWS-based PDUs", + Long: `Connects to one or more PDUs with a JAWS interface to collect hardware inventory.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + log.Error().Msg("no PDU hosts provided") + return + } + + if username == "" || password == "" { + log.Error().Msg("--username and --password are required for PDU collection") + return + } + + collection := make([]*pdu.PDUInventory, 0) + for _, host := range args { + log.Info().Msgf("Collecting from PDU: %s", host) + config := jaws.CrawlerConfig{ + URI: host, + Username: username, + Password: password, + } + + inventory, err := jaws.CrawlPDU(config) + if err != nil { + log.Error().Err(err).Msgf("failed to crawl PDU %s", host) + continue + } + collection = append(collection, inventory) + } + + output, err := json.MarshalIndent(collection, "", " ") + if err != nil { + log.Error().Err(err).Msgf("failed to marshal PDU collection to JSON") + } + fmt.Println(string(output)) + }, +} + +func init() { + PduCmd.AddCommand(pduCollectCmd) + + pduCollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the PDU username") + pduCollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the PDU password") +} diff --git a/cmd/pdu.go b/cmd/pdu.go new file mode 100644 index 0000000..bfe00f2 --- /dev/null +++ b/cmd/pdu.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var PduCmd = &cobra.Command{ + Use: "pdu", + Short: "Perform actions on Power Distribution Units (PDUs)", + Long: `A collection of commands to discover and manage PDUs that may not use the Redfish protocol.`, +} + +func init() { + rootCmd.AddCommand(PduCmd) +} diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go new file mode 100644 index 0000000..a4faba2 --- /dev/null +++ b/pkg/jaws/pdu-crawler.go @@ -0,0 +1,52 @@ +package jaws + +import ( + "fmt" + "net/http" + "time" + + "github.com/OpenCHAMI/magellan/pkg/pdu" +) + +type CrawlerConfig struct { + URI string + Username string + Password string + Insecure bool + Timeout time.Duration +} + +// CrawlPDU connects to a single JAWS PDU and collects its inventory. +func CrawlPDU(config CrawlerConfig) (*pdu.PDUInventory, error) { + client := &http.Client{ + Timeout: config.Timeout, + } + _ = client + + inventory := &pdu.PDUInventory{ + Hostname: config.URI, + } + + // 1. Get System Info + // Should call /jaws/config/info/system + // Create a temporary struct to unmarshal the response + // and then populate PDU inventory, like is done in CSM. + + // 2. Get Outlet Status + // Should call /jaws/outlet/status or similar endpoint + // It will return a list of outlets to parse + + fmt.Printf("Crawling JAWS PDU at %s...\n", config.URI) + + return inventory, nil +} + +/* +func getSystemInfo(client *http.Client, config CrawlerConfig) (*SystemInfo, error) { + // GET to /jaws/config/info/system +} + +func getOutletStatus(client *http.Client, config CrawlerConfig) ([]pdu.PDUOutlet, error) { + // GET to /jaws/outlet/status +} +*/ diff --git a/pkg/pdu/README.md b/pkg/pdu/README.md new file mode 100644 index 0000000..b077941 --- /dev/null +++ b/pkg/pdu/README.md @@ -0,0 +1 @@ +./magellan pdu collect x3000m0 --username admn --password admn diff --git a/pkg/pdu/pdu-inventory.go b/pkg/pdu/pdu-inventory.go new file mode 100644 index 0000000..4e02972 --- /dev/null +++ b/pkg/pdu/pdu-inventory.go @@ -0,0 +1,15 @@ +package pdu + +type PDUOutlet struct { + ID string `json:"id"` // e.g., "35" or "BA35" + Name string `json:"name"` // e.g., "Link1_Outlet_35" + PowerState string `json:"power_state"` // e.g., "ON" or "OFF" +} + +type PDUInventory struct { + Hostname string `json:"hostname"` + Model string `json:"model,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` + FirmwareVersion string `json:"firmware_version,omitempty"` + Outlets []PDUOutlet `json:"outlets"` +} From 56522a3f296065952821c7f7214e23ad18c3609d Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 6 Jun 2025 13:25:19 -0700 Subject: [PATCH 02/32] Add some code to query PDUs --- pkg/jaws/pdu-crawler.go | 86 ++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go index a4faba2..a73c67c 100644 --- a/pkg/jaws/pdu-crawler.go +++ b/pkg/jaws/pdu-crawler.go @@ -1,11 +1,15 @@ package jaws import ( + "crypto/tls" + "encoding/json" "fmt" + "io" "net/http" "time" "github.com/OpenCHAMI/magellan/pkg/pdu" + "github.com/rs/zerolog/log" ) type CrawlerConfig struct { @@ -16,37 +20,75 @@ type CrawlerConfig struct { Timeout time.Duration } -// CrawlPDU connects to a single JAWS PDU and collects its inventory. +// JawsOutlet represents the structure of a single outlet object +type JawsOutlet struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + Current float32 `json:"current"` + Voltage float32 `json:"voltage"` + ActivePower int `json:"active_power"` +} + func CrawlPDU(config CrawlerConfig) (*pdu.PDUInventory, error) { - client := &http.Client{ - Timeout: config.Timeout, + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.Insecure}, + } + client := &http.Client{ + Timeout: config.Timeout, + Transport: transport, } - _ = client inventory := &pdu.PDUInventory{ Hostname: config.URI, } - // 1. Get System Info - // Should call /jaws/config/info/system - // Create a temporary struct to unmarshal the response - // and then populate PDU inventory, like is done in CSM. + targetURL := fmt.Sprintf("https://%s/jaws/monitor/outlets", config.URI) - // 2. Get Outlet Status - // Should call /jaws/outlet/status or similar endpoint - // It will return a list of outlets to parse + req, err := http.NewRequest("GET", targetURL, nil) + if err != nil { + log.Error().Err(err).Msg("failed to create new HTTP request") + return nil, err + } - fmt.Printf("Crawling JAWS PDU at %s...\n", config.URI) + 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) + 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)) + 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") + return nil, err + } + log.Debug().RawJSON("response_body", body).Msg("received response from JAWS") + + var rawOutlets []JawsOutlet + if err := json.Unmarshal(body, &rawOutlets); err != nil { + log.Error().Err(err).Msg("failed to unmarshal JAWS outlet data") + return nil, err + } + + for _, rawOutlet := range rawOutlets { + outlet := pdu.PDUOutlet{ + ID: rawOutlet.ID, + Name: rawOutlet.Name, + PowerState: rawOutlet.State, + } + inventory.Outlets = append(inventory.Outlets, outlet) + } + + log.Info().Msgf("successfully collected inventory for %d outlets from %s", len(inventory.Outlets), config.URI) return inventory, nil } - -/* -func getSystemInfo(client *http.Client, config CrawlerConfig) (*SystemInfo, error) { - // GET to /jaws/config/info/system -} - -func getOutletStatus(client *http.Client, config CrawlerConfig) ([]pdu.PDUOutlet, error) { - // GET to /jaws/outlet/status -} -*/ From a9d16f50cf85e7506bcde0936a599e341fa4d2a6 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 6 Jun 2025 13:41:21 -0700 Subject: [PATCH 03/32] Querying PDU working --- cmd/pdu-collect.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index c8e0590..ecc9e68 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -32,6 +32,7 @@ var pduCollectCmd = &cobra.Command{ URI: host, Username: username, Password: password, + Insecure: true, } inventory, err := jaws.CrawlPDU(config) From 266b3df0b7c9ed1f453393a0f32bc816cadf9f4d Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 6 Jun 2025 13:53:53 -0700 Subject: [PATCH 04/32] 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 { From ae26dac32842a0568bc35006805c2d50232ebe3b Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 6 Jun 2025 13:59:27 -0700 Subject: [PATCH 05/32] Fix typo --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 216b48f..6b4f1e8 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -34,7 +34,7 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { "RedfishURL": redfishURL, "ComponentEndpointType": "ComponentEndpointOutlet", "RedfishOutletInfo": map[string]any{ - "Name": outlet.Name, // Directly from inventory + "Name": outlet.Name, "Actions": map[string]any{ "#Outlet.PowerControl": map[string]any{ "PowerState@Redfish.AllowableValues": []string{"On", "Off"}, From 28231c69121c6b7b085d45f1469b2bc54425a69d Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Mon, 9 Jun 2025 14:49:42 -0700 Subject: [PATCH 06/32] add a mock flag for running on system without a PDU --- cmd/pdu-collect.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 6b4f1e8..0eb6753 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -10,6 +10,8 @@ import ( "github.com/spf13/cobra" ) +var mock bool + func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { smdRecords := make([]map[string]any, 0) @@ -53,6 +55,51 @@ var pduCollectCmd = &cobra.Command{ Short: "Collect inventory from JAWS-based PDUs", Long: `Connects to one or more PDUs with a JAWS interface to collect hardware inventory.`, Run: func(cmd *cobra.Command, args []string) { + if mock { + log.Info().Msg("Running in --mock mode. Generating hardcoded PDU payload to standard output.") + + type PDUInventoryForSMD struct { + Model string `json:"Model"` + SerialNumber string `json:"SerialNumber"` + FirmwareVersion string `json:"FirmwareVersion"` + Outlets []any `json:"Outlets"` + } + type PayloadForSMD struct { + ID string `json:"ID"` + Type string `json:"Type"` + FQDN string `json:"FQDN"` + Hostname string `json:"Hostname"` + Enabled bool `json:"Enabled"` + RediscoverOnUpdate bool `json:"RediscoverOnUpdate"` + PDUInventory PDUInventoryForSMD `json:"PDUInventory"` + } + + mockPayload := PayloadForSMD{ + ID: "x9999m0", + Type: "CabinetPDUController", + FQDN: "x9999m0-rts.mock:8083", + Hostname: "x9999m0-rts.mock:8083", + Enabled: true, + RediscoverOnUpdate: false, + PDUInventory: PDUInventoryForSMD{ + Model: "MOCK-PRO2", + SerialNumber: "MOCK-SN-12345", + FirmwareVersion: "v9.9z", + Outlets: []any{ + map[string]string{"id": "ZA01", "name": "Mock_Server_01", "state": "On", "socket_type": "Cx"}, + map[string]string{"id": "ZA02", "name": "Mock_Server_02", "state": "Off", "socket_type": "Cx"}, + }, + }, + } + + jsonData, err := json.MarshalIndent(mockPayload, "", " ") + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal mock payload") + } + + fmt.Println(string(jsonData)) + return + } if len(args) == 0 { log.Error().Msg("no PDU hosts provided") return @@ -98,4 +145,5 @@ func init() { pduCollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the PDU username") pduCollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the PDU password") + pduCollectCmd.Flags().BoolVar(&mock, "mock", false, "Run in mock mode, sending hardcoded data to SMD") } From c47addedc5c40c3de3b73f8576e3d94afac1a8e6 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Mon, 9 Jun 2025 14:55:07 -0700 Subject: [PATCH 07/32] Turn into an array to match expected format --- cmd/pdu-collect.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 0eb6753..c959d25 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -91,8 +91,9 @@ var pduCollectCmd = &cobra.Command{ }, }, } + payloadCollection := []PayloadForSMD{mockPayload} - jsonData, err := json.MarshalIndent(mockPayload, "", " ") + jsonData, err := json.MarshalIndent(payloadCollection, "", " ") if err != nil { log.Fatal().Err(err).Msg("Failed to marshal mock payload") } From 5d24cbffece93c8d2b3f0401080916895e0af1e1 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Mon, 9 Jun 2025 15:04:43 -0700 Subject: [PATCH 08/32] Remove port numbers --- cmd/pdu-collect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index c959d25..4816c20 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -77,8 +77,8 @@ var pduCollectCmd = &cobra.Command{ mockPayload := PayloadForSMD{ ID: "x9999m0", Type: "CabinetPDUController", - FQDN: "x9999m0-rts.mock:8083", - Hostname: "x9999m0-rts.mock:8083", + FQDN: "x9999m0-rts.mock", + Hostname: "x9999m0-rts.mock", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From 5e28654375c3204c87ccdaa09621d55040a88fed Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Mon, 9 Jun 2025 15:06:02 -0700 Subject: [PATCH 09/32] Cleanup data --- cmd/pdu-collect.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 4816c20..c543fad 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -77,14 +77,14 @@ var pduCollectCmd = &cobra.Command{ mockPayload := PayloadForSMD{ ID: "x9999m0", Type: "CabinetPDUController", - FQDN: "x9999m0-rts.mock", - Hostname: "x9999m0-rts.mock", + FQDN: "x9999m0rts", + Hostname: "x9999m0rts", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ - Model: "MOCK-PRO2", - SerialNumber: "MOCK-SN-12345", - FirmwareVersion: "v9.9z", + Model: "MOCKPRO2", + SerialNumber: "MOCKSN12345", + FirmwareVersion: "v99z", Outlets: []any{ map[string]string{"id": "ZA01", "name": "Mock_Server_01", "state": "On", "socket_type": "Cx"}, map[string]string{"id": "ZA02", "name": "Mock_Server_02", "state": "Off", "socket_type": "Cx"}, From 58af92d2979964dcd96254a3f246d728b7b00c6f Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Mon, 9 Jun 2025 15:29:29 -0700 Subject: [PATCH 10/32] Change xname --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index c543fad..db332a5 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,7 +75,7 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x9999m0", + ID: "x3000c0b0", Type: "CabinetPDUController", FQDN: "x9999m0rts", Hostname: "x9999m0rts", From ddf2b3deb4b2ce9c28c33a975f41ad4b557d8856 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 13:45:48 -0700 Subject: [PATCH 11/32] Update some var names --- cmd/pdu-collect.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index db332a5..03dc368 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,10 +75,10 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x3000c0b0", + ID: "x1000c1s7b0n0", Type: "CabinetPDUController", - FQDN: "x9999m0rts", - Hostname: "x9999m0rts", + FQDN: "172.24.0.3", + Hostname: "x1000c1s7b0n0", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From 7e50184d7a3bdbd491a8ce555313b55fed8350fc Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 13:48:21 -0700 Subject: [PATCH 12/32] Update some var names --- cmd/pdu-collect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 03dc368..9d41379 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,10 +75,10 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x1000c1s7b0n0", + ID: "x1000c1s7b0", Type: "CabinetPDUController", FQDN: "172.24.0.3", - Hostname: "x1000c1s7b0n0", + Hostname: "x1000c1s7b0", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From 2692913f913e715f382932ef9e817216c74296a0 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 13:51:08 -0700 Subject: [PATCH 13/32] Update some var names --- cmd/pdu-collect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 9d41379..8cb1ddc 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,10 +75,10 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x1000c1s7b0", + ID: "x1000c1s7b0n0", Type: "CabinetPDUController", FQDN: "172.24.0.3", - Hostname: "x1000c1s7b0", + Hostname: "x9999m0rts", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From fb5bb1ef0707489c943497f93ebbadb12f5bacde Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 14:08:28 -0700 Subject: [PATCH 14/32] Update some var names --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 8cb1ddc..3ee7e34 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -78,7 +78,7 @@ var pduCollectCmd = &cobra.Command{ ID: "x1000c1s7b0n0", Type: "CabinetPDUController", FQDN: "172.24.0.3", - Hostname: "x9999m0rts", + Hostname: "172.24.0.3", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From 3dad7e517bda8e39d65237b0102de05066791f84 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 14:15:49 -0700 Subject: [PATCH 15/32] Switch xname --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 3ee7e34..b6755d6 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,7 +75,7 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x1000c1s7b0n0", + ID: "x1000c1s7b0", Type: "CabinetPDUController", FQDN: "172.24.0.3", Hostname: "172.24.0.3", From c5ba98eacd453f2b69e987524e0892df4d7c8c9f Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 15:13:05 -0700 Subject: [PATCH 16/32] Switch to node --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index b6755d6..a4f27ba 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -76,7 +76,7 @@ var pduCollectCmd = &cobra.Command{ mockPayload := PayloadForSMD{ ID: "x1000c1s7b0", - Type: "CabinetPDUController", + Type: "Node", FQDN: "172.24.0.3", Hostname: "172.24.0.3", Enabled: true, From 8891041e505cdf22e5735ca673c8382152862eaa Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 16:42:03 -0700 Subject: [PATCH 17/32] Switch to old xname --- cmd/pdu-collect.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index a4f27ba..df0251e 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,10 +75,10 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x1000c1s7b0", + ID: "x3000m0", Type: "Node", - FQDN: "172.24.0.3", - Hostname: "172.24.0.3", + FQDN: "x3000m0asd", + Hostname: "x3000m0asd", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From b256011e8ae82b5753bab10ee5540165e4029277 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 10 Jun 2025 16:48:53 -0700 Subject: [PATCH 18/32] Change ID --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index df0251e..247f316 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,7 +75,7 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x3000m0", + ID: "x1000c1s7b0", Type: "Node", FQDN: "x3000m0asd", Hostname: "x3000m0asd", From a074a8aa6bfd68a15b355db3008eb3cde0529a12 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Wed, 11 Jun 2025 14:04:05 -0700 Subject: [PATCH 19/32] Switch back to pdu xname --- cmd/pdu-collect.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 247f316..8555855 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -75,10 +75,10 @@ var pduCollectCmd = &cobra.Command{ } mockPayload := PayloadForSMD{ - ID: "x1000c1s7b0", + ID: "x3000m0", Type: "Node", - FQDN: "x3000m0asd", - Hostname: "x3000m0asd", + FQDN: "x3000m0", + Hostname: "x3000m0", Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ From 4e4d00803d83a4b9bcb0f94b98b7e4006cbd5539 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 12 Jun 2025 14:02:45 -0700 Subject: [PATCH 20/32] Update info to reflect real PDU --- cmd/pdu-collect.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 8555855..8fd951e 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -82,12 +82,9 @@ var pduCollectCmd = &cobra.Command{ Enabled: true, RediscoverOnUpdate: false, PDUInventory: PDUInventoryForSMD{ - Model: "MOCKPRO2", - SerialNumber: "MOCKSN12345", - FirmwareVersion: "v99z", Outlets: []any{ - map[string]string{"id": "ZA01", "name": "Mock_Server_01", "state": "On", "socket_type": "Cx"}, - map[string]string{"id": "ZA02", "name": "Mock_Server_02", "state": "Off", "socket_type": "Cx"}, + map[string]string{"id": "BA35", "name": "Link1_Outlet_35", "state": "On", "socket_type": "Cx"}, + map[string]string{"id": "BA36", "name": "Link1_Outlet_36", "state": "Off", "socket_type": "Cx"}, }, }, } From 67f9d3e2b34d89ac57933d990d7f406e4bd076ac Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 12 Jun 2025 14:54:23 -0700 Subject: [PATCH 21/32] Change format --- cmd/pdu-collect.go | 52 ++++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 8fd951e..d0a0ab5 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -13,41 +13,29 @@ import ( var mock bool func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { - smdRecords := make([]map[string]any, 0) - - rtsHostname := fmt.Sprintf("%s-rts:8083", inventory.Hostname) - pduBank := "B" - + smdOutlets := make([]map[string]any, 0) 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, - "Actions": map[string]any{ - "#Outlet.PowerControl": map[string]any{ - "PowerState@Redfish.AllowableValues": []string{"On", "Off"}, - "target": powerControlTarget, - }, - }, - }, + rawOutlet := map[string]any{ + "id": outlet.ID, + "name": outlet.Name, + "state": outlet.State, + "socket_type": outlet.SocketType, } - smdRecords = append(smdRecords, record) + smdOutlets = append(smdOutlets, rawOutlet) } - return smdRecords + + pduRecord := map[string]any{ + "ID": inventory.Hostname, + "Type": "Node", + "FQDN": inventory.Hostname, + "Hostname": inventory.Hostname, + "Enabled": true, + "RediscoverOnUpdate": false, + "PDUInventory": map[string]any{ + "Outlets": smdOutlets, + }, + } + return []map[string]any{pduRecord} } var pduCollectCmd = &cobra.Command{ From 1b80bef5face4e3ab247d0079323aa6f9b4fd065 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 12 Jun 2025 15:04:20 -0700 Subject: [PATCH 22/32] Fix socket type --- cmd/pdu-collect.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index d0a0ab5..fd077bd 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -18,8 +18,8 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { rawOutlet := map[string]any{ "id": outlet.ID, "name": outlet.Name, - "state": outlet.State, - "socket_type": outlet.SocketType, + "state": outlet.PowerState, + "socket_type": "Cx", } smdOutlets = append(smdOutlets, rawOutlet) } @@ -35,6 +35,7 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { "Outlets": smdOutlets, }, } + return []map[string]any{pduRecord} } From 20492fe03e7ca12b609f1248b25907648fac612e Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 12 Jun 2025 15:11:19 -0700 Subject: [PATCH 23/32] Remove mock functionality --- cmd/pdu-collect.go | 44 -------------------------------------------- 1 file changed, 44 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index fd077bd..700a868 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -44,49 +44,6 @@ var pduCollectCmd = &cobra.Command{ Short: "Collect inventory from JAWS-based PDUs", Long: `Connects to one or more PDUs with a JAWS interface to collect hardware inventory.`, Run: func(cmd *cobra.Command, args []string) { - if mock { - log.Info().Msg("Running in --mock mode. Generating hardcoded PDU payload to standard output.") - - type PDUInventoryForSMD struct { - Model string `json:"Model"` - SerialNumber string `json:"SerialNumber"` - FirmwareVersion string `json:"FirmwareVersion"` - Outlets []any `json:"Outlets"` - } - type PayloadForSMD struct { - ID string `json:"ID"` - Type string `json:"Type"` - FQDN string `json:"FQDN"` - Hostname string `json:"Hostname"` - Enabled bool `json:"Enabled"` - RediscoverOnUpdate bool `json:"RediscoverOnUpdate"` - PDUInventory PDUInventoryForSMD `json:"PDUInventory"` - } - - mockPayload := PayloadForSMD{ - ID: "x3000m0", - Type: "Node", - FQDN: "x3000m0", - Hostname: "x3000m0", - Enabled: true, - RediscoverOnUpdate: false, - PDUInventory: PDUInventoryForSMD{ - Outlets: []any{ - map[string]string{"id": "BA35", "name": "Link1_Outlet_35", "state": "On", "socket_type": "Cx"}, - map[string]string{"id": "BA36", "name": "Link1_Outlet_36", "state": "Off", "socket_type": "Cx"}, - }, - }, - } - payloadCollection := []PayloadForSMD{mockPayload} - - jsonData, err := json.MarshalIndent(payloadCollection, "", " ") - if err != nil { - log.Fatal().Err(err).Msg("Failed to marshal mock payload") - } - - fmt.Println(string(jsonData)) - return - } if len(args) == 0 { log.Error().Msg("no PDU hosts provided") return @@ -132,5 +89,4 @@ func init() { pduCollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the PDU username") pduCollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the PDU password") - pduCollectCmd.Flags().BoolVar(&mock, "mock", false, "Run in mock mode, sending hardcoded data to SMD") } From 6b3d31708624aa4e2a2f3e67e179b2eb32939f3a Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Thu, 12 Jun 2025 15:25:48 -0700 Subject: [PATCH 24/32] Remove system info --- pkg/jaws/pdu-crawler.go | 48 ---------------------------------------- pkg/pdu/pdu-inventory.go | 7 ++---- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go index ca4e27d..6cb3161 100644 --- a/pkg/jaws/pdu-crawler.go +++ b/pkg/jaws/pdu-crawler.go @@ -30,44 +30,6 @@ 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{ @@ -82,16 +44,6 @@ 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) diff --git a/pkg/pdu/pdu-inventory.go b/pkg/pdu/pdu-inventory.go index 4e02972..3b0441d 100644 --- a/pkg/pdu/pdu-inventory.go +++ b/pkg/pdu/pdu-inventory.go @@ -7,9 +7,6 @@ type PDUOutlet struct { } type PDUInventory struct { - Hostname string `json:"hostname"` - Model string `json:"model,omitempty"` - SerialNumber string `json:"serial_number,omitempty"` - FirmwareVersion string `json:"firmware_version,omitempty"` - Outlets []PDUOutlet `json:"outlets"` + Hostname string `json:"hostname"` + Outlets []PDUOutlet `json:"outlets"` } From dc09b7051d18d43bd9c09d63ffabacfd60118519 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 13 Jun 2025 08:56:40 -0700 Subject: [PATCH 25/32] Switch to standard xname --- cmd/pdu-collect.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 700a868..883a149 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -3,6 +3,8 @@ package cmd import ( "encoding/json" "fmt" + "strings" + "unicode" "github.com/OpenCHAMI/magellan/pkg/jaws" "github.com/OpenCHAMI/magellan/pkg/pdu" @@ -10,16 +12,32 @@ import ( "github.com/spf13/cobra" ) -var mock bool - func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { smdOutlets := make([]map[string]any, 0) for _, outlet := range inventory.Outlets { + var letterPart, numberPart string + splitIndex := strings.IndexFunc(outlet.ID, unicode.IsDigit) + + if splitIndex == -1 { + log.Warn().Msgf("could not parse outlet ID format for '%s', skipping outlet", outlet.ID) + continue + } + + letterPart = outlet.ID[:splitIndex] + numberPart = outlet.ID[splitIndex:] + + var pValue int + if len(letterPart) > 1 { + pValue = int(unicode.ToUpper(rune(letterPart[1])) - 'A') + } + + newIDSuffix := fmt.Sprintf("p%dv%s", pValue, numberPart) + rawOutlet := map[string]any{ - "id": outlet.ID, + "id": newIDSuffix, // Pass the newly formatted suffix to SMD "name": outlet.Name, "state": outlet.PowerState, - "socket_type": "Cx", + "socket_type": "Cx", // This seems to be static } smdOutlets = append(smdOutlets, rawOutlet) } From 940e7fee2a235a4c50a36acf9ad80126a692e0b1 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 13 Jun 2025 09:26:10 -0700 Subject: [PATCH 26/32] Add socket type and construct correct xname --- cmd/pdu-collect.go | 10 +++++----- pkg/jaws/pdu-crawler.go | 2 ++ pkg/pdu/pdu-inventory.go | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 883a149..55a9e2e 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -22,7 +22,6 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { log.Warn().Msgf("could not parse outlet ID format for '%s', skipping outlet", outlet.ID) continue } - letterPart = outlet.ID[:splitIndex] numberPart = outlet.ID[splitIndex:] @@ -30,14 +29,15 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { if len(letterPart) > 1 { pValue = int(unicode.ToUpper(rune(letterPart[1])) - 'A') } - - newIDSuffix := fmt.Sprintf("p%dv%s", pValue, numberPart) + + idSuffix := fmt.Sprintf("p%dv%s", pValue, numberPart) rawOutlet := map[string]any{ - "id": newIDSuffix, // Pass the newly formatted suffix to SMD + "original_id": outlet.ID, + "id_suffix": idSuffix, "name": outlet.Name, "state": outlet.PowerState, - "socket_type": "Cx", // This seems to be static + "socket_type": outlet.SocketType, } smdOutlets = append(smdOutlets, rawOutlet) } diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go index 6cb3161..03bf441 100644 --- a/pkg/jaws/pdu-crawler.go +++ b/pkg/jaws/pdu-crawler.go @@ -25,6 +25,7 @@ type JawsOutlet struct { ID string `json:"id"` Name string `json:"name"` State string `json:"state"` + SocketType string `json:"socket_type"` Current float32 `json:"current"` Voltage float32 `json:"voltage"` ActivePower int `json:"active_power"` @@ -85,6 +86,7 @@ func CrawlPDU(config CrawlerConfig) (*pdu.PDUInventory, error) { ID: rawOutlet.ID, Name: rawOutlet.Name, PowerState: rawOutlet.State, + SocketType: rawOutlet.SocketType, } inventory.Outlets = append(inventory.Outlets, outlet) } diff --git a/pkg/pdu/pdu-inventory.go b/pkg/pdu/pdu-inventory.go index 3b0441d..71e956a 100644 --- a/pkg/pdu/pdu-inventory.go +++ b/pkg/pdu/pdu-inventory.go @@ -4,6 +4,7 @@ type PDUOutlet struct { ID string `json:"id"` // e.g., "35" or "BA35" Name string `json:"name"` // e.g., "Link1_Outlet_35" PowerState string `json:"power_state"` // e.g., "ON" or "OFF" + SocketType string `json:"socket_type"` } type PDUInventory struct { From ad500d086dc3bfab1d1190c479bbe2b657789c5b Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 13 Jun 2025 10:05:48 -0700 Subject: [PATCH 27/32] Fix xname numbering --- cmd/pdu-collect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 55a9e2e..0c53e03 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -27,7 +27,7 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { var pValue int if len(letterPart) > 1 { - pValue = int(unicode.ToUpper(rune(letterPart[1])) - 'A') + pValue = int(unicode.ToUpper(rune(letterPart[0])) - 'A') } idSuffix := fmt.Sprintf("p%dv%s", pValue, numberPart) From 4971f62dba7ec55ae6f4d97b849fe6e04161191e Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Fri, 13 Jun 2025 11:27:44 -0700 Subject: [PATCH 28/32] Switch to magellan collect pdu --- cmd/pdu-collect.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 0c53e03..0520c75 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -26,10 +26,10 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { numberPart = outlet.ID[splitIndex:] var pValue int - if len(letterPart) > 1 { + if len(letterPart) > 0 { pValue = int(unicode.ToUpper(rune(letterPart[0])) - 'A') } - + idSuffix := fmt.Sprintf("p%dv%s", pValue, numberPart) rawOutlet := map[string]any{ @@ -57,8 +57,8 @@ func transformToSMDFormat(inventory *pdu.PDUInventory) []map[string]any { return []map[string]any{pduRecord} } -var pduCollectCmd = &cobra.Command{ - Use: "collect [hosts...]", +var pduCmd = &cobra.Command{ + Use: "pdu [hosts...]", Short: "Collect inventory from JAWS-based PDUs", Long: `Connects to one or more PDUs with a JAWS interface to collect hardware inventory.`, Run: func(cmd *cobra.Command, args []string) { @@ -103,8 +103,8 @@ var pduCollectCmd = &cobra.Command{ } func init() { - PduCmd.AddCommand(pduCollectCmd) + pduCmd.Flags().StringVarP(&username, "username", "u", "", "Set the PDU username") + pduCmd.Flags().StringVarP(&password, "password", "p", "", "Set the PDU password") - pduCollectCmd.Flags().StringVarP(&username, "username", "u", "", "Set the PDU username") - pduCollectCmd.Flags().StringVarP(&password, "password", "p", "", "Set the PDU password") + CollectCmd.AddCommand(pduCmd) } From c379d248ffcc7255d378c89f5d01d866a2d2b88a Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 17 Jun 2025 11:06:54 -0700 Subject: [PATCH 29/32] Remove unused PDU command --- cmd/pdu.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 cmd/pdu.go diff --git a/cmd/pdu.go b/cmd/pdu.go deleted file mode 100644 index bfe00f2..0000000 --- a/cmd/pdu.go +++ /dev/null @@ -1,15 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var PduCmd = &cobra.Command{ - Use: "pdu", - Short: "Perform actions on Power Distribution Units (PDUs)", - Long: `A collection of commands to discover and manage PDUs that may not use the Redfish protocol.`, -} - -func init() { - rootCmd.AddCommand(PduCmd) -} From 5960c987d433016daef649d1812dad13cbe011aa Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 17 Jun 2025 11:10:52 -0700 Subject: [PATCH 30/32] Add some examples --- cmd/pdu-collect.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/pdu-collect.go b/cmd/pdu-collect.go index 0520c75..ba281ed 100644 --- a/cmd/pdu-collect.go +++ b/cmd/pdu-collect.go @@ -61,6 +61,12 @@ var pduCmd = &cobra.Command{ Use: "pdu [hosts...]", Short: "Collect inventory from JAWS-based PDUs", Long: `Connects to one or more PDUs with a JAWS interface to collect hardware inventory.`, + Example: ` + // Collect inventory from a single PDU using credentials + magellan collect pdu x3000m0 --username admin --password inital0 + + // Collect from multiple PDUs and send to SMD + magellan collect pdu x3000m0 x3000m1 -u admin -p initial0 | ./magellan send `, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { log.Error().Msg("no PDU hosts provided") From 7d7ccf33b94391db1cc314358453a2670e1afb85 Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 17 Jun 2025 11:18:51 -0700 Subject: [PATCH 31/32] Update README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d9eee43..2c10e28 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,18 @@ This maintains the original behavior of passing the `--host` flag to `collect` w > └── 1747550498.yaml > ``` +### PDU Inventory Collection + +In addition to collecting Redfish inventory from BMCs, `magellan` can also collect inventory from Power Distribution Units (PDUs) that expose a JAWS-style API. The `collect` command has a `pdu` subcommand for this purpose. + +The command connects to the specified PDU host(s), gathers all outlet information, and transforms it into the nested JSON format required by SMD in OpenCHAMI. + +The most common workflow is to collect from the PDU and pipe the JSON output directly to the `magellan send` command, which then POSTs the data to a running SMD instance. + +```bash +# Collect from a PDU and pipe the output directly to a local SMD instance +./magellan collect pdu pdu.example.com --username admin --password "pdu-password" | ./magellan send http://localhost:27779 + ### Managing Secrets When connecting to an array of BMC nodes, some nodes may have different secret credentials than the rest. These secrets can be stored and used automatically by `magellan` when performing a `collect` or a `crawl`. All secrets are encrypted and are only accessible using the same `MASTER_KEY` as when stored originally. From 068478d9bcc0cf9b418915c10d5f4bacf09459ec Mon Sep 17 00:00:00 2001 From: Ben McDonald Date: Tue, 17 Jun 2025 11:21:42 -0700 Subject: [PATCH 32/32] Run gofmt --- pkg/jaws/pdu-crawler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/jaws/pdu-crawler.go b/pkg/jaws/pdu-crawler.go index 03bf441..3cafa61 100644 --- a/pkg/jaws/pdu-crawler.go +++ b/pkg/jaws/pdu-crawler.go @@ -25,7 +25,7 @@ type JawsOutlet struct { ID string `json:"id"` Name string `json:"name"` State string `json:"state"` - SocketType string `json:"socket_type"` + SocketType string `json:"socket_type"` Current float32 `json:"current"` Voltage float32 `json:"voltage"` ActivePower int `json:"active_power"`