package receivers import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "maps" "net/http" "strconv" "strings" "sync" "time" lp "github.com/ClusterCockpit/cc-energy-manager/pkg/cc-message" cclog "github.com/ClusterCockpit/cc-metric-collector/pkg/ccLogger" "github.com/ClusterCockpit/cc-metric-collector/pkg/hostlist" mp "github.com/ClusterCockpit/cc-metric-collector/pkg/messageProcessor" // See: https://pkg.go.dev/github.com/stmcginnis/gofish "github.com/stmcginnis/gofish" "github.com/stmcginnis/gofish/common" "github.com/stmcginnis/gofish/redfish" ) type RedfishReceiverClientConfig struct { // Hostname the redfish service belongs to Hostname string // is metric excluded globally or per client isExcluded map[string](bool) doPowerMetric bool doProcessorMetrics bool doSensors bool doThermalMetrics bool skipProcessorMetricsURL map[string]bool // readSensorURLs stores for each chassis ID a list of sensor URLs to read readSensorURLs map[string][]string gofish gofish.ClientConfig mp mp.MessageProcessor } // RedfishReceiver configuration: type RedfishReceiver struct { receiver config struct { defaultReceiverConfig fanout int Interval time.Duration HttpTimeout time.Duration // Client config for each redfish service ClientConfigs []RedfishReceiverClientConfig } done chan bool // channel to finish / stop redfish receiver wg sync.WaitGroup // wait group for redfish receiver } // deleteEmptyTags removes tags or meta data tags with empty value func deleteEmptyTags(tags map[string]string) { maps.DeleteFunc( tags, func(key string, value string) bool { return value == "" }, ) } // setMetricValue sets the value entry in the fields map func setMetricValue(value any) map[string]interface{} { return map[string]interface{}{ "value": value, } } // sendMetric sends the metric through the sink channel func (r *RedfishReceiver) sendMetric(mp mp.MessageProcessor, name string, tags map[string]string, meta map[string]string, value any, timestamp time.Time) { deleteEmptyTags(tags) deleteEmptyTags(meta) y, err := lp.NewMessage(name, tags, meta, setMetricValue(value), timestamp) if err == nil { mc, err := mp.ProcessMessage(y) if err == nil && mc != nil { m, err := r.mp.ProcessMessage(mc) if err == nil && m != nil { r.sink <- m } } } } // readSensors reads sensors from a redfish device // See: https://redfish.dmtf.org/schemas/v1/Sensor.json // Redfish URI: /redfish/v1/Chassis/{ChassisId}/Sensors/{SensorId} func (r *RedfishReceiver) readSensors( clientConfig *RedfishReceiverClientConfig, chassis *redfish.Chassis) error { writeTemperatureSensor := func(sensor *redfish.Sensor) { tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "sensor_id": sensor.ID, // The area or device to which this sensor measurement applies "temperature_physical_context": string(sensor.PhysicalContext), // Name "temperature_name": sensor.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "Temperature", "unit": "degC", } r.sendMetric(clientConfig.mp, "temperature", tags, meta, sensor.Reading, time.Now()) } writeFanSpeedSensor := func(sensor *redfish.Sensor) { tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "sensor_id": sensor.ID, // The area or device to which this sensor measurement applies "fan_physical_context": string(sensor.PhysicalContext), // Name "fan_name": sensor.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "FanSpeed", "unit": string(sensor.ReadingUnits), } r.sendMetric(clientConfig.mp, "fan_speed", tags, meta, sensor.Reading, time.Now()) } writePowerSensor := func(sensor *redfish.Sensor) { // Set tags tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "sensor_id": sensor.ID, // The area or device to which this sensor measurement applies "power_physical_context": string(sensor.PhysicalContext), // Name "power_name": sensor.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "Energy", "unit": "watts", } r.sendMetric(clientConfig.mp, "power", tags, meta, sensor.Reading, time.Now()) } if _, ok := clientConfig.readSensorURLs[chassis.ID]; !ok { // First time run of read sensors for this chassis clientConfig.readSensorURLs[chassis.ID] = make([]string, 0) // Get sensor information for this chassis sensors, err := chassis.Sensors() if err != nil { return fmt.Errorf("readSensors: chassis.Sensors() failed: %v", err) } // Skip empty sensors information if sensors == nil { return nil } for _, sensor := range sensors { // Skip all sensors which are not in enabled state or which are unhealthy if sensor.Status.State != common.EnabledState || sensor.Status.Health != common.OKHealth { continue } // Skip sensors with missing readings units or type if sensor.ReadingUnits == "" || sensor.ReadingType == "" { continue } // Power readings if (sensor.ReadingType == redfish.PowerReadingType && sensor.ReadingUnits == "Watts") || (sensor.ReadingType == redfish.CurrentReadingType && sensor.ReadingUnits == "Watts") { if clientConfig.isExcluded["power"] { continue } clientConfig.readSensorURLs[chassis.ID] = append(clientConfig.readSensorURLs[chassis.ID], sensor.ODataID) writePowerSensor(sensor) continue } // Fan speed readings if (sensor.ReadingType == redfish.AirFlowReadingType && sensor.ReadingUnits == "RPM") || (sensor.ReadingType == redfish.AirFlowReadingType && sensor.ReadingUnits == "Percent") { // Skip, when fan_speed metric is excluded if clientConfig.isExcluded["fan_speed"] { continue } clientConfig.readSensorURLs[chassis.ID] = append(clientConfig.readSensorURLs[chassis.ID], sensor.ODataID) writeFanSpeedSensor(sensor) } // Temperature readings if sensor.ReadingType == redfish.TemperatureReadingType && sensor.ReadingUnits == "C" { if clientConfig.isExcluded["temperature"] { continue } clientConfig.readSensorURLs[chassis.ID] = append(clientConfig.readSensorURLs[chassis.ID], sensor.ODataID) writeTemperatureSensor(sensor) continue } } } else { common.CollectCollection( func(uri string) { sensor, err := redfish.GetSensor(chassis.GetClient(), uri) if err != nil { cclog.ComponentError(r.name, "redfish.GetSensor() for uri '", uri, "' failed") } // Power readings if (sensor.ReadingType == redfish.PowerReadingType && sensor.ReadingUnits == "Watts") || (sensor.ReadingType == redfish.CurrentReadingType && sensor.ReadingUnits == "Watts") { writePowerSensor(sensor) return } // Fan speed readings if (sensor.ReadingType == redfish.AirFlowReadingType && sensor.ReadingUnits == "RPM") || (sensor.ReadingType == redfish.AirFlowReadingType && sensor.ReadingUnits == "Percent") { writeFanSpeedSensor(sensor) return } // Temperature readings if sensor.ReadingType == redfish.TemperatureReadingType && sensor.ReadingUnits == "C" { writeTemperatureSensor(sensor) return } }, clientConfig.readSensorURLs[chassis.ID]) } return nil } // readThermalMetrics reads thermal metrics from a redfish device // See: https://redfish.dmtf.org/schemas/v1/Thermal.json // Redfish URI: /redfish/v1/Chassis/{ChassisId}/Thermal // -> deprecated in favor of the ThermalSubsystem schema // -> on Lenovo servers /redfish/v1/Chassis/{ChassisId}/ThermalSubsystem/ThermalMetrics links to /redfish/v1/Chassis/{ChassisId}/Sensors/{SensorId} func (r *RedfishReceiver) readThermalMetrics( clientConfig *RedfishReceiverClientConfig, chassis *redfish.Chassis) error { // Get thermal information for each chassis thermal, err := chassis.Thermal() if err != nil { return fmt.Errorf("readMetrics: chassis.Thermal() failed: %v", err) } // Skip empty thermal information if thermal == nil { return nil } timestamp := time.Now() for _, temperature := range thermal.Temperatures { // Skip, when temperature metric is excluded if clientConfig.isExcluded["temperature"] { break } // Skip all temperatures which are not in enabled state if temperature.Status.State != "" && temperature.Status.State != common.EnabledState { continue } tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "temperature_id": temperature.ID, // MemberID shall uniquely identify the member within the collection. For // services supporting Redfish v1.6 or higher, this value shall be the // zero-based array index. "temperature_member_id": temperature.MemberID, // PhysicalContext shall be a description of the affected device or region // within the chassis to which this temperature measurement applies "temperature_physical_context": string(temperature.PhysicalContext), // Name "temperature_name": temperature.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "Temperature", "unit": "degC", } // ReadingCelsius shall be the current value of the temperature sensor's reading. value := temperature.ReadingCelsius r.sendMetric(clientConfig.mp, "temperature", tags, meta, value, timestamp) } for _, fan := range thermal.Fans { // Skip, when fan_speed metric is excluded if clientConfig.isExcluded["fan_speed"] { break } // Skip all fans which are not in enabled state if fan.Status.State != common.EnabledState { continue } tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "fan_id": fan.ID, // MemberID shall uniquely identify the member within the collection. For // services supporting Redfish v1.6 or higher, this value shall be the // zero-based array index. "fan_member_id": fan.MemberID, // PhysicalContext shall be a description of the affected device or region // within the chassis to which this fan is associated "fan_physical_context": string(fan.PhysicalContext), // Name "fan_name": fan.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "FanSpeed", "unit": string(fan.ReadingUnits), } r.sendMetric(clientConfig.mp, "fan_speed", tags, meta, fan.Reading, timestamp) } return nil } // readPowerMetrics reads power metrics from a redfish device // See: https://redfish.dmtf.org/schemas/v1/Power.json // Redfish URI: /redfish/v1/Chassis/{ChassisId}/Power // -> deprecated in favor of the PowerSubsystem schema func (r *RedfishReceiver) readPowerMetrics( clientConfig *RedfishReceiverClientConfig, chassis *redfish.Chassis) error { // Get power information for each chassis power, err := chassis.Power() if err != nil { return fmt.Errorf("readMetrics: chassis.Power() failed: %v", err) } // Skip empty power information if power == nil { return nil } timestamp := time.Now() // Read min, max and average consumed watts for each power control for _, pc := range power.PowerControl { // Skip all power controls which are not in enabled state if pc.Status.State != "" && pc.Status.State != common.EnabledState { continue } // Map of collected metrics metrics := make(map[string]float32) // PowerConsumedWatts shall represent the actual power being consumed (in // Watts) by the chassis if !clientConfig.isExcluded["consumed_watts"] { metrics["consumed_watts"] = pc.PowerConsumedWatts } // AverageConsumedWatts shall represent the // average power level that occurred averaged over the last IntervalInMin // minutes. if !clientConfig.isExcluded["average_consumed_watts"] { metrics["average_consumed_watts"] = pc.PowerMetrics.AverageConsumedWatts } // MinConsumedWatts shall represent the // minimum power level in watts that occurred within the last // IntervalInMin minutes. if !clientConfig.isExcluded["min_consumed_watts"] { metrics["min_consumed_watts"] = pc.PowerMetrics.MinConsumedWatts } // MaxConsumedWatts shall represent the // maximum power level in watts that occurred within the last // IntervalInMin minutes if !clientConfig.isExcluded["max_consumed_watts"] { metrics["max_consumed_watts"] = pc.PowerMetrics.MaxConsumedWatts } // IntervalInMin shall represent the time interval (or window), in minutes, // in which the PowerMetrics properties are measured over. // Should be an integer, but some Dell implementations return as a float intervalInMin := strconv.FormatFloat( float64(pc.PowerMetrics.IntervalInMin), 'f', -1, 32) // Set tags tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "node", // ChassisType shall indicate the physical form factor for the type of chassis "chassis_typ": string(chassis.ChassisType), // Chassis name "chassis_name": chassis.Name, // ID uniquely identifies the resource "power_control_id": pc.ID, // MemberID shall uniquely identify the member within the collection. For // services supporting Redfish v1.6 or higher, this value shall be the // zero-based array index. "power_control_member_id": pc.MemberID, // PhysicalContext shall be a description of the affected device(s) or region // within the chassis to which this power control applies. "power_control_physical_context": string(pc.PhysicalContext), // Name "power_control_name": pc.Name, } // Set meta data tags meta := map[string]string{ "source": r.name, "group": "Energy", "interval_in_minutes": intervalInMin, "unit": "watts", } for name, value := range metrics { r.sendMetric(clientConfig.mp, name, tags, meta, value, timestamp) } } return nil } // readProcessorMetrics reads processor metrics from a redfish device // See: https://redfish.dmtf.org/schemas/v1/ProcessorMetrics.json // Redfish URI: /redfish/v1/Systems/{ComputerSystemId}/Processors/{ProcessorId}/ProcessorMetrics func (r *RedfishReceiver) readProcessorMetrics( clientConfig *RedfishReceiverClientConfig, processor *redfish.Processor) error { timestamp := time.Now() // URL to processor metrics URL := processor.ODataID + "/ProcessorMetrics" // Skip previously detected non existing URLs if clientConfig.skipProcessorMetricsURL[URL] { return nil } resp, err := processor.GetClient().Get(URL) if err != nil { // Skip non existing URLs if statusCode := err.(*common.Error).HTTPReturnedStatusCode; statusCode == http.StatusNotFound { clientConfig.skipProcessorMetricsURL[URL] = true return nil } return fmt.Errorf("processor.GetClient().Get(%v) failed: %+w", URL, err) } var processorMetrics struct { common.Entity ODataType string `json:"@odata.type"` ODataEtag string `json:"@odata.etag"` Description string `json:"Description"` // This property shall contain the power, in watts, that the processor has consumed. ConsumedPowerWatt float32 `json:"ConsumedPowerWatt"` // This property shall contain the temperature, in Celsius, of the processor. TemperatureCelsius float32 `json:"TemperatureCelsius"` } body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read response body for processor metrics: %+w", err) } err = json.Unmarshal(body, &processorMetrics) if err != nil { return fmt.Errorf( "unable to unmarshal JSON='%s' for processor metrics: %+w", string(body), err, ) } // Set tags tags := map[string]string{ "hostname": clientConfig.Hostname, "type": "socket", // ProcessorType shall contain the string which identifies the type of processor contained in this Socket "processor_typ": string(processor.ProcessorType), // Processor name "processor_name": processor.Name, // ID uniquely identifies the resource "processor_id": processor.ID, } // Set meta data tags metaPower := map[string]string{ "source": r.name, "group": "Energy", "unit": "watts", } namePower := "consumed_power" if !clientConfig.isExcluded[namePower] && // Some servers return "ConsumedPowerWatt":65535 instead of "ConsumedPowerWatt":null processorMetrics.ConsumedPowerWatt != 65535 { r.sendMetric(clientConfig.mp, namePower, tags, metaPower, processorMetrics.ConsumedPowerWatt, timestamp) } // Set meta data tags metaThermal := map[string]string{ "source": r.name, "group": "Temperature", "unit": "degC", } nameThermal := "temperature" if !clientConfig.isExcluded[nameThermal] { r.sendMetric(clientConfig.mp, nameThermal, tags, metaThermal, processorMetrics.TemperatureCelsius, timestamp) } return nil } // readMetrics reads redfish thermal, power and processor metrics from the redfish device // configured in clientConfig func (r *RedfishReceiver) readMetrics(clientConfig *RedfishReceiverClientConfig) error { // Connect to redfish service c, err := gofish.Connect(clientConfig.gofish) if err != nil { return fmt.Errorf( "readMetrics: gofish.Connect({Username: %v, Endpoint: %v, BasicAuth: %v, HttpTimeout: %v, HttpInsecure: %v}) failed: %v", clientConfig.gofish.Username, clientConfig.gofish.Endpoint, clientConfig.gofish.BasicAuth, clientConfig.gofish.HTTPClient.Timeout, clientConfig.gofish.HTTPClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify, err) } defer c.Logout() // Create a session, when required if _, err = c.GetSession(); err != nil { c, err = c.CloneWithSession() if err != nil { return fmt.Errorf("readMetrics: Failed to create a session: %+w", err) } } // Get all chassis managed by this service isChassisListRequired := clientConfig.doSensors || clientConfig.doThermalMetrics || clientConfig.doPowerMetric var chassisList []*redfish.Chassis if isChassisListRequired { chassisList, err = c.Service.Chassis() if err != nil { return fmt.Errorf("readMetrics: c.Service.Chassis() failed: %v", err) } } // Get all computer systems managed by this service isComputerSystemListRequired := clientConfig.doProcessorMetrics var computerSystemList []*redfish.ComputerSystem if isComputerSystemListRequired { computerSystemList, err = c.Service.Systems() if err != nil { return fmt.Errorf("readMetrics: c.Service.Systems() failed: %v", err) } } // Read sensors if clientConfig.doSensors { for _, chassis := range chassisList { err := r.readSensors(clientConfig, chassis) if err != nil { return err } } } // read thermal metrics if clientConfig.doThermalMetrics { for _, chassis := range chassisList { err := r.readThermalMetrics(clientConfig, chassis) if err != nil { return err } } } // read power metrics if clientConfig.doPowerMetric { for _, chassis := range chassisList { err = r.readPowerMetrics(clientConfig, chassis) if err != nil { return err } } } // read processor metrics if clientConfig.doProcessorMetrics { // loop for all computer systems for _, system := range computerSystemList { // loop for all processors processors, err := system.Processors() if err != nil { return fmt.Errorf("readMetrics: system.Processors() failed: %v", err) } for _, processor := range processors { err := r.readProcessorMetrics(clientConfig, processor) if err != nil { return err } } } } return nil } // doReadMetrics reads metrics from all configure redfish devices. // To compensate latencies of the Redfish devices a fanout is used. func (r *RedfishReceiver) doReadMetric() { // Create wait group and input channel for workers var workerWaitGroup sync.WaitGroup workerInput := make(chan *RedfishReceiverClientConfig, r.config.fanout) // Create worker go routines for i := 0; i < r.config.fanout; i++ { // Increment worker wait group counter workerWaitGroup.Add(1) go func() { // Decrement worker wait group counter defer workerWaitGroup.Done() // Read power metrics for each client config for clientConfig := range workerInput { err := r.readMetrics(clientConfig) if err != nil { cclog.ComponentError(r.name, err) } } }() } // Distribute client configs to workers for i := range r.config.ClientConfigs { // Check done channel status select { case workerInput <- &r.config.ClientConfigs[i]: case <-r.done: // process done event // Stop workers, clear channel and wait for all workers to finish close(workerInput) for range workerInput { } workerWaitGroup.Wait() return } } // Stop workers and wait for all workers to finish close(workerInput) workerWaitGroup.Wait() } // Start starts the redfish receiver func (r *RedfishReceiver) Start() { cclog.ComponentDebug(r.name, "START") // Start redfish receiver r.wg.Add(1) go func() { defer r.wg.Done() // Create ticker ticker := time.NewTicker(r.config.Interval) defer ticker.Stop() for { r.doReadMetric() select { case tickerTime := <-ticker.C: // Check if we missed the ticker event if since := time.Since(tickerTime); since > 5*time.Second { cclog.ComponentInfo(r.name, "Missed ticker event for more then", since) } // process ticker event -> continue continue case <-r.done: // process done event return } } }() cclog.ComponentDebug(r.name, "STARTED") } // Close closes the redfish receiver func (r *RedfishReceiver) Close() { cclog.ComponentDebug(r.name, "CLOSE") // Send the signal and wait close(r.done) r.wg.Wait() cclog.ComponentDebug(r.name, "DONE") } // NewRedfishReceiver creates a new instance of the redfish receiver // Initialize the receiver by giving it a name and reading in the config JSON func NewRedfishReceiver(name string, config json.RawMessage) (Receiver, error) { var err error r := new(RedfishReceiver) // Config options from config file configJSON := struct { Type string `json:"type"` MessageProcessor json.RawMessage `json:"process_messages,omitempty"` // Maximum number of simultaneous redfish connections (default: 64) Fanout int `json:"fanout,omitempty"` // How often the redfish power metrics should be read and send to the sink (default: 30 s) IntervalString string `json:"interval,omitempty"` // Control whether a client verifies the server's certificate // (default: true == do not verify server's certificate) HttpInsecure bool `json:"http_insecure,omitempty"` // Time limit for requests made by this HTTP client (default: 10 s) HttpTimeoutString string `json:"http_timeout,omitempty"` // Default client username, password and endpoint Username *string `json:"username"` // User name to authenticate with Password *string `json:"password"` // Password to use for authentication Endpoint *string `json:"endpoint"` // URL of the redfish service // Globally disable collection of power, processor or thermal metrics DisablePowerMetrics bool `json:"disable_power_metrics"` DisableProcessorMetrics bool `json:"disable_processor_metrics"` DisableSensors bool `json:"disable_sensors"` DisableThermalMetrics bool `json:"disable_thermal_metrics"` // Globally excluded metrics ExcludeMetrics []string `json:"exclude_metrics,omitempty"` ClientConfigs []struct { HostList string `json:"host_list"` // List of hosts with the same client configuration Username *string `json:"username"` // User name to authenticate with Password *string `json:"password"` // Password to use for authentication Endpoint *string `json:"endpoint"` // URL of the redfish service // Per client disable collection of power,processor or thermal metrics DisablePowerMetrics bool `json:"disable_power_metrics"` DisableProcessorMetrics bool `json:"disable_processor_metrics"` DisableSensors bool `json:"disable_sensors"` DisableThermalMetrics bool `json:"disable_thermal_metrics"` // Per client excluded metrics ExcludeMetrics []string `json:"exclude_metrics,omitempty"` MessageProcessor json.RawMessage `json:"process_messages,omitempty"` } `json:"client_config"` }{ // Set defaults values // Allow overwriting these defaults by reading config JSON Fanout: 64, IntervalString: "30s", HttpTimeoutString: "10s", HttpInsecure: true, } // Set name r.name = fmt.Sprintf("RedfishReceiver(%s)", name) // Create done channel r.done = make(chan bool) // Read the redfish receiver specific JSON config if len(config) > 0 { d := json.NewDecoder(bytes.NewReader(config)) d.DisallowUnknownFields() if err := d.Decode(&configJSON); err != nil { cclog.ComponentError(r.name, "Error reading config:", err.Error()) return nil, err } } p, err := mp.NewMessageProcessor() if err != nil { return nil, fmt.Errorf("initialization of message processor failed: %v", err.Error()) } r.mp = p if len(r.config.MessageProcessor) > 0 { err = r.mp.FromConfigJSON(r.config.MessageProcessor) if err != nil { return nil, fmt.Errorf("failed parsing JSON for message processor: %v", err.Error()) } } // Convert interval string representation to duration r.config.Interval, err = time.ParseDuration(configJSON.IntervalString) if err != nil { err := fmt.Errorf( "failed to parse duration string interval='%s': %w", configJSON.IntervalString, err, ) cclog.Error(r.name, err) return nil, err } // HTTP timeout duration r.config.HttpTimeout, err = time.ParseDuration(configJSON.HttpTimeoutString) if err != nil { err := fmt.Errorf( "failed to parse duration string http_timeout='%s': %w", configJSON.HttpTimeoutString, err, ) cclog.Error(r.name, err) return nil, err } // Create new http client customTransport := http.DefaultTransport.(*http.Transport).Clone() customTransport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: configJSON.HttpInsecure, } httpClient := &http.Client{ Timeout: r.config.HttpTimeout, Transport: customTransport, } // Initialize client configurations r.config.ClientConfigs = make([]RedfishReceiverClientConfig, 0) // Create client config from JSON config for i := range configJSON.ClientConfigs { clientConfigJSON := &configJSON.ClientConfigs[i] // Redfish endpoint var endpoint_pattern string if clientConfigJSON.Endpoint != nil { endpoint_pattern = *clientConfigJSON.Endpoint } else if configJSON.Endpoint != nil { endpoint_pattern = *configJSON.Endpoint } else { err := fmt.Errorf("client config number %v requires endpoint", i) cclog.ComponentError(r.name, err) return nil, err } // Redfish username var username string if clientConfigJSON.Username != nil { username = *clientConfigJSON.Username } else if configJSON.Username != nil { username = *configJSON.Username } else { err := fmt.Errorf("client config number %v requires username", i) cclog.ComponentError(r.name, err) return nil, err } // Redfish password var password string if clientConfigJSON.Password != nil { password = *clientConfigJSON.Password } else if configJSON.Password != nil { password = *configJSON.Password } else { err := fmt.Errorf("client config number %v requires password", i) cclog.ComponentError(r.name, err) return nil, err } // Which metrics should be collected doPowerMetric := !(configJSON.DisablePowerMetrics || clientConfigJSON.DisablePowerMetrics) doProcessorMetrics := !(configJSON.DisableProcessorMetrics || clientConfigJSON.DisableProcessorMetrics) doSensors := !(configJSON.DisableSensors || clientConfigJSON.DisableSensors) doThermalMetrics := !(configJSON.DisableThermalMetrics || clientConfigJSON.DisableThermalMetrics) // Is metrics excluded globally or per client isExcluded := make(map[string]bool) for _, key := range clientConfigJSON.ExcludeMetrics { isExcluded[key] = true } for _, key := range configJSON.ExcludeMetrics { isExcluded[key] = true } p, err = mp.NewMessageProcessor() if err != nil { cclog.ComponentError(r.name, err.Error()) return nil, err } if len(clientConfigJSON.MessageProcessor) > 0 { err = p.FromConfigJSON(clientConfigJSON.MessageProcessor) if err != nil { cclog.ComponentError(r.name, err.Error()) return nil, err } } hostList, err := hostlist.Expand(clientConfigJSON.HostList) if err != nil { err := fmt.Errorf("client config number %d failed to parse host list %s: %v", i, clientConfigJSON.HostList, err) cclog.ComponentError(r.name, err) return nil, err } for _, host := range hostList { // Endpoint of the redfish service endpoint := strings.Replace(endpoint_pattern, "%h", host, -1) r.config.ClientConfigs = append( r.config.ClientConfigs, RedfishReceiverClientConfig{ Hostname: host, isExcluded: isExcluded, doPowerMetric: doPowerMetric, doProcessorMetrics: doProcessorMetrics, doSensors: doSensors, doThermalMetrics: doThermalMetrics, skipProcessorMetricsURL: make(map[string]bool), readSensorURLs: map[string][]string{}, gofish: gofish.ClientConfig{ Username: username, Password: password, Endpoint: endpoint, HTTPClient: httpClient, }, mp: p, }) } } // Compute parallel fanout to use numClients := len(r.config.ClientConfigs) r.config.fanout = configJSON.Fanout if numClients < r.config.fanout { r.config.fanout = numClients } // Check that at least on client config exists if numClients == 0 { err := fmt.Errorf("at least one client config is required") cclog.ComponentError(r.name, err) return nil, err } // Check for duplicate client configurations isDuplicate := make(map[string]bool) for i := range r.config.ClientConfigs { host := r.config.ClientConfigs[i].Hostname if isDuplicate[host] { err := fmt.Errorf("found duplicate client config for host %s", host) cclog.ComponentError(r.name, err) return nil, err } isDuplicate[host] = true } // Give some basic info about redfish receiver status cclog.ComponentInfo(r.name, "Monitoring", numClients, "clients") cclog.ComponentInfo(r.name, "Monitoring interval:", r.config.Interval) cclog.ComponentInfo(r.name, "Monitoring parallel fanout:", r.config.fanout) return r, nil }