1 #include "psu_manager.hpp"
2 
3 #include "utility.hpp"
4 
5 #include <fmt/format.h>
6 #include <sys/types.h>
7 #include <unistd.h>
8 
9 #include <regex>
10 
11 using namespace phosphor::logging;
12 
13 namespace phosphor::power::manager
14 {
15 
16 constexpr auto IBMCFFPSInterface =
17     "xyz.openbmc_project.Configuration.IBMCFFPSConnector";
18 constexpr auto i2cBusProp = "I2CBus";
19 constexpr auto i2cAddressProp = "I2CAddress";
20 constexpr auto psuNameProp = "Name";
21 constexpr auto presLineName = "NamedPresenceGpio";
22 
23 constexpr auto supportedConfIntf =
24     "xyz.openbmc_project.Configuration.SupportedConfiguration";
25 
26 PSUManager::PSUManager(sdbusplus::bus::bus& bus, const sdeventplus::Event& e) :
27     bus(bus)
28 {
29     // Subscribe to InterfacesAdded before doing a property read, otherwise
30     // the interface could be created after the read attempt but before the
31     // match is created.
32     entityManagerIfacesAddedMatch = std::make_unique<sdbusplus::bus::match_t>(
33         bus,
34         sdbusplus::bus::match::rules::interfacesAdded() +
35             sdbusplus::bus::match::rules::sender(
36                 "xyz.openbmc_project.EntityManager"),
37         std::bind(&PSUManager::entityManagerIfaceAdded, this,
38                   std::placeholders::_1));
39     getPSUConfiguration();
40     getSystemProperties();
41 
42     using namespace sdeventplus;
43     auto interval = std::chrono::milliseconds(1000);
44     timer = std::make_unique<utility::Timer<ClockId::Monotonic>>(
45         e, std::bind(&PSUManager::analyze, this), interval);
46 
47     validationTimer = std::make_unique<utility::Timer<ClockId::Monotonic>>(
48         e, std::bind(&PSUManager::validateConfig, this));
49 
50     try
51     {
52         powerConfigGPIO = createGPIO("power-config-full-load");
53     }
54     catch (const std::exception& e)
55     {
56         // Ignore error, GPIO may not be implemented in this system.
57         powerConfigGPIO = nullptr;
58     }
59 
60     // Subscribe to power state changes
61     powerService = util::getService(POWER_OBJ_PATH, POWER_IFACE, bus);
62     powerOnMatch = std::make_unique<sdbusplus::bus::match_t>(
63         bus,
64         sdbusplus::bus::match::rules::propertiesChanged(POWER_OBJ_PATH,
65                                                         POWER_IFACE),
66         [this](auto& msg) { this->powerStateChanged(msg); });
67 
68     initialize();
69 }
70 
71 void PSUManager::getPSUConfiguration()
72 {
73     using namespace phosphor::power::util;
74     auto depth = 0;
75     auto objects = getSubTree(bus, "/", IBMCFFPSInterface, depth);
76 
77     psus.clear();
78 
79     // I should get a map of objects back.
80     // Each object will have a path, a service, and an interface.
81     // The interface should match the one passed into this function.
82     for (const auto& [path, services] : objects)
83     {
84         auto service = services.begin()->first;
85 
86         if (path.empty() || service.empty())
87         {
88             continue;
89         }
90 
91         // For each object in the array of objects, I want to get properties
92         // from the service, path, and interface.
93         auto properties =
94             getAllProperties(bus, path, IBMCFFPSInterface, service);
95 
96         getPSUProperties(properties);
97     }
98 
99     if (psus.empty())
100     {
101         // Interface or properties not found. Let the Interfaces Added callback
102         // process the information once the interfaces are added to D-Bus.
103         log<level::INFO>(fmt::format("No power supplies to monitor").c_str());
104     }
105 }
106 
107 void PSUManager::getPSUProperties(util::DbusPropertyMap& properties)
108 {
109     // From passed in properties, I want to get: I2CBus, I2CAddress,
110     // and Name. Create a power supply object, using Name to build the inventory
111     // path.
112     const auto basePSUInvPath =
113         "/xyz/openbmc_project/inventory/system/chassis/motherboard/powersupply";
114     uint64_t* i2cbus = nullptr;
115     uint64_t* i2caddr = nullptr;
116     std::string* psuname = nullptr;
117     std::string* preslineptr = nullptr;
118 
119     for (const auto& property : properties)
120     {
121         try
122         {
123             if (property.first == i2cBusProp)
124             {
125                 i2cbus = std::get_if<uint64_t>(&properties[i2cBusProp]);
126             }
127             else if (property.first == i2cAddressProp)
128             {
129                 i2caddr = std::get_if<uint64_t>(&properties[i2cAddressProp]);
130             }
131             else if (property.first == psuNameProp)
132             {
133                 psuname = std::get_if<std::string>(&properties[psuNameProp]);
134             }
135             else if (property.first == presLineName)
136             {
137                 preslineptr =
138                     std::get_if<std::string>(&properties[presLineName]);
139             }
140         }
141         catch (const std::exception& e)
142         {}
143     }
144 
145     if ((i2cbus) && (i2caddr) && (psuname) && (!psuname->empty()))
146     {
147         std::string invpath = basePSUInvPath;
148         invpath.push_back(psuname->back());
149         std::string presline = "";
150 
151         log<level::DEBUG>(fmt::format("Inventory Path: {}", invpath).c_str());
152 
153         if (nullptr != preslineptr)
154         {
155             presline = *preslineptr;
156         }
157 
158         auto invMatch =
159             std::find_if(psus.begin(), psus.end(), [&invpath](auto& psu) {
160                 return psu->getInventoryPath() == invpath;
161             });
162         if (invMatch != psus.end())
163         {
164             // This power supply has the same inventory path as the one with
165             // information just added to D-Bus.
166             // Changes to GPIO line name unlikely, so skip checking.
167             // Changes to the I2C bus and address unlikely, as that would
168             // require corresponding device tree updates.
169             // Return out to avoid duplicate object creation.
170             return;
171         }
172 
173         log<level::DEBUG>(
174             fmt::format("make PowerSupply bus: {} addr: {} presline: {}",
175                         *i2cbus, *i2caddr, presline)
176                 .c_str());
177         auto psu = std::make_unique<PowerSupply>(bus, invpath, *i2cbus,
178                                                  *i2caddr, presline);
179         psus.emplace_back(std::move(psu));
180     }
181 
182     if (psus.empty())
183     {
184         log<level::INFO>(fmt::format("No power supplies to monitor").c_str());
185     }
186 }
187 
188 void PSUManager::populateSysProperties(const util::DbusPropertyMap& properties)
189 {
190     try
191     {
192         auto propIt = properties.find("SupportedType");
193         if (propIt == properties.end())
194         {
195             return;
196         }
197         const std::string* type = std::get_if<std::string>(&(propIt->second));
198         if ((type == nullptr) || (*type != "PowerSupply"))
199         {
200             return;
201         }
202 
203         propIt = properties.find("SupportedModel");
204         if (propIt == properties.end())
205         {
206             return;
207         }
208         const std::string* model = std::get_if<std::string>(&(propIt->second));
209         if (model == nullptr)
210         {
211             return;
212         }
213 
214         sys_properties sys;
215         propIt = properties.find("RedundantCount");
216         if (propIt != properties.end())
217         {
218             const uint64_t* count = std::get_if<uint64_t>(&(propIt->second));
219             if (count != nullptr)
220             {
221                 sys.powerSupplyCount = *count;
222             }
223         }
224         propIt = properties.find("InputVoltage");
225         if (propIt != properties.end())
226         {
227             const std::vector<uint64_t>* voltage =
228                 std::get_if<std::vector<uint64_t>>(&(propIt->second));
229             if (voltage != nullptr)
230             {
231                 sys.inputVoltage = *voltage;
232             }
233         }
234 
235         // The PowerConfigFullLoad is an optional property, default it to false
236         // since that's the default value of the power-config-full-load GPIO.
237         sys.powerConfigFullLoad = false;
238         propIt = properties.find("PowerConfigFullLoad");
239         if (propIt != properties.end())
240         {
241             const bool* fullLoad = std::get_if<bool>(&(propIt->second));
242             if (fullLoad != nullptr)
243             {
244                 sys.powerConfigFullLoad = *fullLoad;
245             }
246         }
247 
248         supportedConfigs.emplace(*model, sys);
249     }
250     catch (const std::exception& e)
251     {}
252 }
253 
254 void PSUManager::getSystemProperties()
255 {
256 
257     try
258     {
259         util::DbusSubtree subtree =
260             util::getSubTree(bus, INVENTORY_OBJ_PATH, supportedConfIntf, 0);
261         if (subtree.empty())
262         {
263             throw std::runtime_error("Supported Configuration Not Found");
264         }
265 
266         for (const auto& [objPath, services] : subtree)
267         {
268             std::string service = services.begin()->first;
269             if (objPath.empty() || service.empty())
270             {
271                 continue;
272             }
273             auto properties = util::getAllProperties(
274                 bus, objPath, supportedConfIntf, service);
275             populateSysProperties(properties);
276         }
277     }
278     catch (const std::exception& e)
279     {
280         // Interface or property not found. Let the Interfaces Added callback
281         // process the information once the interfaces are added to D-Bus.
282     }
283 }
284 
285 void PSUManager::entityManagerIfaceAdded(sdbusplus::message::message& msg)
286 {
287     try
288     {
289         sdbusplus::message::object_path objPath;
290         std::map<std::string, std::map<std::string, util::DbusVariant>>
291             interfaces;
292         msg.read(objPath, interfaces);
293 
294         auto itIntf = interfaces.find(supportedConfIntf);
295         if (itIntf != interfaces.cend())
296         {
297             populateSysProperties(itIntf->second);
298         }
299 
300         itIntf = interfaces.find(IBMCFFPSInterface);
301         if (itIntf != interfaces.cend())
302         {
303             log<level::INFO>(
304                 fmt::format("InterfacesAdded for: {}", IBMCFFPSInterface)
305                     .c_str());
306             getPSUProperties(itIntf->second);
307         }
308 
309         // Call to validate the psu configuration if the power is on and both
310         // the IBMCFFPSConnector and SupportedConfiguration interfaces have been
311         // processed
312         if (powerOn && !psus.empty() && !supportedConfigs.empty())
313         {
314             validationTimer->restartOnce(validationTimeout);
315         }
316     }
317     catch (const std::exception& e)
318     {
319         // Ignore, the property may be of a different type than expected.
320     }
321 }
322 
323 void PSUManager::powerStateChanged(sdbusplus::message::message& msg)
324 {
325     int32_t state = 0;
326     std::string msgSensor;
327     std::map<std::string, std::variant<int32_t>> msgData;
328     msg.read(msgSensor, msgData);
329 
330     // Check if it was the Present property that changed.
331     auto valPropMap = msgData.find("state");
332     if (valPropMap != msgData.end())
333     {
334         state = std::get<int32_t>(valPropMap->second);
335 
336         // Power is on when state=1. Clear faults.
337         if (state)
338         {
339             powerOn = true;
340             validationTimer->restartOnce(validationTimeout);
341             clearFaults();
342             setPowerConfigGPIO();
343         }
344         else
345         {
346             powerOn = false;
347             runValidateConfig = true;
348         }
349     }
350 }
351 
352 void PSUManager::createError(const std::string& faultName,
353                              std::map<std::string, std::string>& additionalData)
354 {
355     using namespace sdbusplus::xyz::openbmc_project;
356     constexpr auto loggingObjectPath = "/xyz/openbmc_project/logging";
357     constexpr auto loggingCreateInterface =
358         "xyz.openbmc_project.Logging.Create";
359 
360     try
361     {
362         additionalData["_PID"] = std::to_string(getpid());
363 
364         auto service =
365             util::getService(loggingObjectPath, loggingCreateInterface, bus);
366 
367         if (service.empty())
368         {
369             log<level::ERR>("Unable to get logging manager service");
370             return;
371         }
372 
373         auto method = bus.new_method_call(service.c_str(), loggingObjectPath,
374                                           loggingCreateInterface, "Create");
375 
376         auto level = Logging::server::Entry::Level::Error;
377         method.append(faultName, level, additionalData);
378 
379         auto reply = bus.call(method);
380     }
381     catch (const std::exception& e)
382     {
383         log<level::ERR>(
384             fmt::format(
385                 "Failed creating event log for fault {} due to error {}",
386                 faultName, e.what())
387                 .c_str());
388     }
389 }
390 
391 void PSUManager::analyze()
392 {
393     for (auto& psu : psus)
394     {
395         psu->analyze();
396     }
397 
398     if (powerOn)
399     {
400         std::map<std::string, std::string> additionalData;
401 
402         for (auto& psu : psus)
403         {
404             additionalData.clear();
405             // TODO: Fault priorities #918
406             if (!psu->isFaultLogged() && !psu->isPresent())
407             {
408                 std::map<std::string, std::string> requiredPSUsData;
409                 auto requiredPSUsPresent = hasRequiredPSUs(requiredPSUsData);
410                 if (!requiredPSUsPresent)
411                 {
412                     additionalData.merge(requiredPSUsData);
413                     // Create error for power supply missing.
414                     additionalData["CALLOUT_INVENTORY_PATH"] =
415                         psu->getInventoryPath();
416                     additionalData["CALLOUT_PRIORITY"] = "H";
417                     createError(
418                         "xyz.openbmc_project.Power.PowerSupply.Error.Missing",
419                         additionalData);
420                 }
421                 psu->setFaultLogged();
422             }
423             else if (!psu->isFaultLogged() && psu->isFaulted())
424             {
425                 // Add STATUS_WORD and STATUS_MFR last response, in padded
426                 // hexadecimal format.
427                 additionalData["STATUS_WORD"] =
428                     fmt::format("{:#04x}", psu->getStatusWord());
429                 additionalData["STATUS_MFR"] =
430                     fmt::format("{:#02x}", psu->getMFRFault());
431                 // If there are faults being reported, they possibly could be
432                 // related to a bug in the firmware version running on the power
433                 // supply. Capture that data into the error as well.
434                 additionalData["FW_VERSION"] = psu->getFWVersion();
435 
436                 if (psu->hasCommFault())
437                 {
438                     additionalData["STATUS_CML"] =
439                         fmt::format("{:#02x}", psu->getStatusCML());
440                     /* Attempts to communicate with the power supply have
441                      * reached there limit. Create an error. */
442                     additionalData["CALLOUT_DEVICE_PATH"] =
443                         psu->getDevicePath();
444 
445                     createError(
446                         "xyz.openbmc_project.Power.PowerSupply.Error.CommFault",
447                         additionalData);
448 
449                     psu->setFaultLogged();
450                 }
451                 else if ((psu->hasInputFault() || psu->hasVINUVFault()))
452                 {
453                     // Include STATUS_INPUT for input faults.
454                     additionalData["STATUS_INPUT"] =
455                         fmt::format("{:#02x}", psu->getStatusInput());
456 
457                     /* The power supply location might be needed if the input
458                      * fault is due to a problem with the power supply itself.
459                      * Include the inventory path with a call out priority of
460                      * low.
461                      */
462                     additionalData["CALLOUT_INVENTORY_PATH"] =
463                         psu->getInventoryPath();
464                     additionalData["CALLOUT_PRIORITY"] = "L";
465                     createError("xyz.openbmc_project.Power.PowerSupply.Error."
466                                 "InputFault",
467                                 additionalData);
468                     psu->setFaultLogged();
469                 }
470                 else if (psu->hasVoutOVFault())
471                 {
472                     // Include STATUS_VOUT for Vout faults.
473                     additionalData["STATUS_VOUT"] =
474                         fmt::format("{:#02x}", psu->getStatusVout());
475 
476                     additionalData["CALLOUT_INVENTORY_PATH"] =
477                         psu->getInventoryPath();
478 
479                     createError(
480                         "xyz.openbmc_project.Power.PowerSupply.Error.Fault",
481                         additionalData);
482 
483                     psu->setFaultLogged();
484                 }
485                 else if (psu->hasIoutOCFault())
486                 {
487                     // Include STATUS_IOUT for Iout faults.
488                     additionalData["STATUS_IOUT"] =
489                         fmt::format("{:#02x}", psu->getStatusIout());
490 
491                     createError(
492                         "xyz.openbmc_project.Power.PowerSupply.Error.IoutOCFault",
493                         additionalData);
494 
495                     psu->setFaultLogged();
496                 }
497                 else if (psu->hasVoutUVFault())
498                 {
499                     // Include STATUS_VOUT for Vout faults.
500                     additionalData["STATUS_VOUT"] =
501                         fmt::format("{:#02x}", psu->getStatusVout());
502 
503                     additionalData["CALLOUT_INVENTORY_PATH"] =
504                         psu->getInventoryPath();
505 
506                     createError(
507                         "xyz.openbmc_project.Power.PowerSupply.Error.Fault",
508                         additionalData);
509 
510                     psu->setFaultLogged();
511                 }
512                 // A fan fault should have priority over a temperature fault,
513                 // since a failed fan may lead to a temperature problem.
514                 else if (psu->hasFanFault())
515                 {
516                     // Include STATUS_TEMPERATURE and STATUS_FANS_1_2
517                     additionalData["STATUS_TEMPERATURE"] =
518                         fmt::format("{:#02x}", psu->getStatusTemperature());
519                     additionalData["STATUS_FANS_1_2"] =
520                         fmt::format("{:#02x}", psu->getStatusFans12());
521 
522                     additionalData["CALLOUT_INVENTORY_PATH"] =
523                         psu->getInventoryPath();
524 
525                     createError(
526                         "xyz.openbmc_project.Power.PowerSupply.Error.FanFault",
527                         additionalData);
528 
529                     psu->setFaultLogged();
530                 }
531                 else if (psu->hasTempFault())
532                 {
533                     // Include STATUS_TEMPERATURE for temperature faults.
534                     additionalData["STATUS_TEMPERATURE"] =
535                         fmt::format("{:#02x}", psu->getStatusTemperature());
536 
537                     additionalData["CALLOUT_INVENTORY_PATH"] =
538                         psu->getInventoryPath();
539 
540                     createError(
541                         "xyz.openbmc_project.Power.PowerSupply.Error.Fault",
542                         additionalData);
543 
544                     psu->setFaultLogged();
545                 }
546                 else if (psu->hasMFRFault())
547                 {
548                     /* This can represent a variety of faults that result in
549                      * calling out the power supply for replacement: Output
550                      * OverCurrent, Output Under Voltage, and potentially other
551                      * faults.
552                      *
553                      * Also plan on putting specific fault in AdditionalData,
554                      * along with register names and register values
555                      * (STATUS_WORD, STATUS_MFR, etc.).*/
556 
557                     additionalData["CALLOUT_INVENTORY_PATH"] =
558                         psu->getInventoryPath();
559 
560                     createError(
561                         "xyz.openbmc_project.Power.PowerSupply.Error.Fault",
562                         additionalData);
563 
564                     psu->setFaultLogged();
565                 }
566                 else if (psu->hasPgoodFault())
567                 {
568                     /* POWER_GOOD# is not low, or OFF is on */
569                     additionalData["CALLOUT_INVENTORY_PATH"] =
570                         psu->getInventoryPath();
571 
572                     createError(
573                         "xyz.openbmc_project.Power.PowerSupply.Error.Fault",
574                         additionalData);
575 
576                     psu->setFaultLogged();
577                 }
578             }
579         }
580     }
581 }
582 
583 void PSUManager::validateConfig()
584 {
585     if (!runValidateConfig || supportedConfigs.empty())
586     {
587         return;
588     }
589 
590     std::map<std::string, std::string> additionalData;
591     auto supported = hasRequiredPSUs(additionalData);
592     if (supported)
593     {
594         runValidateConfig = false;
595         return;
596     }
597 
598     // Validation failed, create an error log.
599     // Return without setting the runValidateConfig flag to false because
600     // it may be that an additional supported configuration interface is
601     // added and we need to validate it to see if it matches this system.
602     createError("xyz.openbmc_project.Power.PowerSupply.Error.NotSupported",
603                 additionalData);
604 }
605 
606 bool PSUManager::hasRequiredPSUs(
607     std::map<std::string, std::string>& additionalData)
608 {
609     std::string model{};
610     if (!validateModelName(model, additionalData))
611     {
612         return false;
613     }
614 
615     auto presentCount =
616         std::count_if(psus.begin(), psus.end(),
617                       [](const auto& psu) { return psu->isPresent(); });
618 
619     // Validate the supported configurations. A system may support more than one
620     // power supply model configuration. Since all configurations need to be
621     // checked, the additional data would contain only the information of the
622     // last configuration that did not match.
623     std::map<std::string, std::string> tmpAdditionalData;
624     for (const auto& config : supportedConfigs)
625     {
626         if (config.first != model)
627         {
628             continue;
629         }
630         if (presentCount != config.second.powerSupplyCount)
631         {
632             tmpAdditionalData.clear();
633             tmpAdditionalData["EXPECTED_COUNT"] =
634                 std::to_string(config.second.powerSupplyCount);
635             tmpAdditionalData["ACTUAL_COUNT"] = std::to_string(presentCount);
636             continue;
637         }
638 
639         bool voltageValidated = true;
640         for (const auto& psu : psus)
641         {
642             if (!psu->isPresent())
643             {
644                 // Only present PSUs report a valid input voltage
645                 continue;
646             }
647 
648             double actualInputVoltage;
649             int inputVoltage;
650             psu->getInputVoltage(actualInputVoltage, inputVoltage);
651 
652             if (std::find(config.second.inputVoltage.begin(),
653                           config.second.inputVoltage.end(),
654                           inputVoltage) == config.second.inputVoltage.end())
655             {
656                 tmpAdditionalData.clear();
657                 tmpAdditionalData["ACTUAL_VOLTAGE"] =
658                     std::to_string(actualInputVoltage);
659                 for (const auto& voltage : config.second.inputVoltage)
660                 {
661                     tmpAdditionalData["EXPECTED_VOLTAGE"] +=
662                         std::to_string(voltage) + " ";
663                 }
664                 tmpAdditionalData["CALLOUT_INVENTORY_PATH"] =
665                     psu->getInventoryPath();
666 
667                 voltageValidated = false;
668                 break;
669             }
670         }
671         if (!voltageValidated)
672         {
673             continue;
674         }
675 
676         return true;
677     }
678 
679     additionalData.insert(tmpAdditionalData.begin(), tmpAdditionalData.end());
680     return false;
681 }
682 
683 bool PSUManager::validateModelName(
684     std::string& model, std::map<std::string, std::string>& additionalData)
685 {
686     // Check that all PSUs have the same model name. Initialize the model
687     // variable with the first PSU name found, then use it as a base to compare
688     // against the rest of the PSUs.
689     model.clear();
690     for (const auto& psu : psus)
691     {
692         auto psuModel = psu->getModelName();
693         if (psuModel.empty())
694         {
695             continue;
696         }
697         if (model.empty())
698         {
699             model = psuModel;
700             continue;
701         }
702         if (psuModel != model)
703         {
704             additionalData["EXPECTED_MODEL"] = model;
705             additionalData["ACTUAL_MODEL"] = psuModel;
706             additionalData["CALLOUT_INVENTORY_PATH"] = psu->getInventoryPath();
707             model.clear();
708             return false;
709         }
710     }
711     return true;
712 }
713 
714 void PSUManager::setPowerConfigGPIO()
715 {
716     if (!powerConfigGPIO)
717     {
718         return;
719     }
720 
721     std::string model{};
722     std::map<std::string, std::string> additionalData;
723     if (!validateModelName(model, additionalData))
724     {
725         return;
726     }
727 
728     auto config = supportedConfigs.find(model);
729     if (config != supportedConfigs.end())
730     {
731         // The power-config-full-load is an open drain GPIO. Set it to low (0)
732         // if the supported configuration indicates that this system model
733         // expects the maximum number of power supplies (full load set to true).
734         // Else, set it to high (1), this is the default.
735         auto powerConfigValue =
736             (config->second.powerConfigFullLoad == true ? 0 : 1);
737         auto flags = gpiod::line_request::FLAG_OPEN_DRAIN;
738         powerConfigGPIO->write(powerConfigValue, flags);
739     }
740 }
741 
742 } // namespace phosphor::power::manager
743