package redfishprofile import "strings" func lenovoProfile() Profile { return staticProfile{ name: "lenovo", priority: 20, safeForFallback: true, matchFn: func(s MatchSignals) int { score := 0 if containsFold(s.SystemManufacturer, "lenovo") || containsFold(s.ChassisManufacturer, "lenovo") { score += 80 } for _, ns := range s.OEMNamespaces { if containsFold(ns, "lenovo") { score += 30 break } } // Lenovo XClarity Controller (XCC) is the BMC product line. if containsFold(s.ServiceRootProduct, "xclarity") || containsFold(s.ServiceRootProduct, "xcc") { score += 30 } return min(score, 100) }, extendAcquisition: func(plan *AcquisitionPlan, _ MatchSignals) { // Lenovo XCC BMC exposes Chassis/1/Sensors with hundreds of individual // sensor member documents (e.g. Chassis/1/Sensors/101L1). These are // not used by any LOGPile parser — thermal/power data is read from // the aggregate Chassis/*/Thermal and Chassis/*/Power endpoints. On // a real server they largely return errors, wasting many minutes. // Lenovo OEM subtrees under Oem/Lenovo/LEDs and Oem/Lenovo/Slots also // enumerate dozens of individual documents not relevant to inventory. ensureSnapshotExcludeContains(plan, "/Sensors/", // individual sensor docs (Chassis/1/Sensors/NNN) "/Oem/Lenovo/LEDs/", // individual LED status entries (~47 per server) "/Oem/Lenovo/Slots/", // individual slot detail entries (~26 per server) "/Oem/Lenovo/Metrics/", // operational metrics, not inventory "/Oem/Lenovo/History", // historical telemetry "/Oem/Lenovo/Configuration", // BMC config service, not inventory "/Oem/Lenovo/DateTimeService", // BMC time service config "/Oem/Lenovo/GroupService", // XCC fleet/group management state "/Oem/Lenovo/Recipients", // alert recipient config "/Oem/Lenovo/RemoteControl", // remote-media/session management "/Oem/Lenovo/RemoteMap", // remote-media mapping config "/Oem/Lenovo/SecureKeyLifecycleService", // key lifecycle/cert config "/Oem/Lenovo/ServerProfile", // profile export/import config "/Oem/Lenovo/ServiceData", // support/service metadata "/Oem/Lenovo/SsoCertificates", // SSO certificate config "/Oem/Lenovo/SystemGuard", // snapshot/history service "/Oem/Lenovo/Watchdogs", // watchdog config "/Oem/Lenovo/ScheduledPower", // power scheduling config "/Oem/Lenovo/BootSettings/BootOrder", // individual boot order lists "/NetworkProtocol/Oem/Lenovo/", // DNS/LDAP/SMTP/SNMP manager config "/PortForwardingMap/", // network port forwarding config "/VirtualMedia/", // virtual media inventory/config, not hardware "/Boot/Certificates", // secure boot certificate stores, not inventory "/ThermalSubsystem/Fans/", // per-fan member docs; replay uses aggregate Thermal only ) // Lenovo XCC BMC is typically slow (p95 latency often 3-5s even under // normal load). Set rate thresholds that don't over-throttle on the // first few requests, and give the ETA estimator a realistic baseline. ensureRatePolicy(plan, AcquisitionRatePolicy{ TargetP95LatencyMS: 2000, ThrottleP95LatencyMS: 4000, MinSnapshotWorkers: 2, MinPrefetchWorkers: 1, DisablePrefetchOnErrors: true, }) ensureETABaseline(plan, AcquisitionETABaseline{ DiscoverySeconds: 15, SnapshotSeconds: 120, PrefetchSeconds: 30, CriticalPlanBSeconds: 40, ProfilePlanBSeconds: 20, }) addPlanNote(plan, "lenovo xcc acquisition extensions enabled: noisy sensor/oem paths excluded from snapshot") }, refineAcquisition: func(resolved *ResolvedAcquisitionPlan, discovered DiscoveredResources, signals MatchSignals) { allowedChassis := lenovoAllowedInventoryChassis(discovered.ChassisPaths, signals.ResourceHints) resolved.SeedPaths = filterLenovoChassisInventoryPaths(resolved.SeedPaths, allowedChassis) resolved.CriticalPaths = filterLenovoChassisInventoryPaths(resolved.CriticalPaths, allowedChassis) resolved.Plan.SeedPaths = filterLenovoChassisInventoryPaths(resolved.Plan.SeedPaths, allowedChassis) resolved.Plan.CriticalPaths = filterLenovoChassisInventoryPaths(resolved.Plan.CriticalPaths, allowedChassis) resolved.Plan.PlanBPaths = filterLenovoChassisInventoryPaths(resolved.Plan.PlanBPaths, allowedChassis) }, } } func lenovoAllowedInventoryChassis(chassisPaths, resourceHints []string) map[string]struct{} { allowed := make(map[string]struct{}, len(chassisPaths)) for _, chassisPath := range chassisPaths { normalized := normalizePath(chassisPath) if normalized == "" { continue } if normalized == "/redfish/v1/Chassis/1" { allowed[normalized] = struct{}{} continue } for _, hint := range resourceHints { hint = normalizePath(hint) if !strings.HasPrefix(hint, normalized+"/") { continue } if lenovoHintLooksLikeChassisInventory(hint) { allowed[normalized] = struct{}{} break } } } return allowed } func lenovoHintLooksLikeChassisInventory(path string) bool { for _, suffix := range []string{ "/Power", "/PowerSubsystem", "/PowerSubsystem/PowerSupplies", "/Thermal", "/ThresholdSensors", "/DiscreteSensors", "/SensorsList", "/NetworkAdapters", "/PCIeDevices", "/Drives", "/Assembly", } { if strings.HasSuffix(path, suffix) || strings.Contains(path, suffix+"/") { return true } } return false } func filterLenovoChassisInventoryPaths(paths []string, allowedChassis map[string]struct{}) []string { if len(paths) == 0 { return nil } out := make([]string, 0, len(paths)) for _, path := range paths { normalized := normalizePath(path) chassis := lenovoPathChassisRoot(normalized) if chassis == "" { out = append(out, normalized) continue } if normalized == chassis { out = append(out, normalized) continue } if _, ok := allowedChassis[chassis]; ok { out = append(out, normalized) } } return dedupeSorted(out) } func lenovoPathChassisRoot(path string) string { const prefix = "/redfish/v1/Chassis/" if !strings.HasPrefix(path, prefix) { return "" } rest := strings.TrimPrefix(path, prefix) if rest == "" { return "" } if idx := strings.IndexByte(rest, '/'); idx >= 0 { return prefix + rest[:idx] } return prefix + rest }