1 /**
2  * Copyright © 2021 IBM Corporation
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 #include "threshold_alarm_logger.hpp"
17 
18 #include "sdbusplus.hpp"
19 
20 #include <fmt/format.h>
21 #include <unistd.h>
22 
23 #include <phosphor-logging/log.hpp>
24 #include <xyz/openbmc_project/Logging/Entry/server.hpp>
25 
26 namespace sensor::monitor
27 {
28 
29 using namespace sdbusplus::xyz::openbmc_project::Logging::server;
30 using namespace phosphor::logging;
31 using namespace phosphor::fan;
32 using namespace phosphor::fan::util;
33 
34 const std::string warningInterface =
35     "xyz.openbmc_project.Sensor.Threshold.Warning";
36 const std::string criticalInterface =
37     "xyz.openbmc_project.Sensor.Threshold.Critical";
38 const std::string perfLossInterface =
39     "xyz.openbmc_project.Sensor.Threshold.PerformanceLoss";
40 constexpr auto loggingService = "xyz.openbmc_project.Logging";
41 constexpr auto loggingPath = "/xyz/openbmc_project/logging";
42 constexpr auto loggingCreateIface = "xyz.openbmc_project.Logging.Create";
43 constexpr auto errorNameBase = "xyz.openbmc_project.Sensor.Threshold.Error.";
44 constexpr auto valueInterface = "xyz.openbmc_project.Sensor.Value";
45 constexpr auto assocInterface = "xyz.openbmc_project.Association";
46 
47 const std::vector<std::string> thresholdIfaceNames{
48     warningInterface, criticalInterface, perfLossInterface};
49 
50 using ErrorData = std::tuple<ErrorName, Entry::Level>;
51 
52 /**
53  * Map of threshold interfaces and alarm properties and values to error data.
54  */
55 const std::map<InterfaceName, std::map<PropertyName, std::map<bool, ErrorData>>>
56     thresholdData{
57 
58         {warningInterface,
59          {{"WarningAlarmHigh",
60            {{true, ErrorData{"WarningHigh", Entry::Level::Warning}},
61             {false,
62              ErrorData{"WarningHighClear", Entry::Level::Informational}}}},
63           {"WarningAlarmLow",
64            {{true, ErrorData{"WarningLow", Entry::Level::Warning}},
65             {false,
66              ErrorData{"WarningLowClear", Entry::Level::Informational}}}}}},
67 
68         {criticalInterface,
69          {{"CriticalAlarmHigh",
70            {{true, ErrorData{"CriticalHigh", Entry::Level::Critical}},
71             {false,
72              ErrorData{"CriticalHighClear", Entry::Level::Informational}}}},
73           {"CriticalAlarmLow",
74            {{true, ErrorData{"CriticalLow", Entry::Level::Critical}},
75             {false,
76              ErrorData{"CriticalLowClear", Entry::Level::Informational}}}}}},
77 
78         {perfLossInterface,
79          {{"PerfLossAlarmHigh",
80            {{true, ErrorData{"PerfLossHigh", Entry::Level::Warning}},
81             {false,
82              ErrorData{"PerfLossHighClear", Entry::Level::Informational}}}},
83           {"PerfLossAlarmLow",
84            {{true, ErrorData{"PerfLossLow", Entry::Level::Warning}},
85             {false,
86              ErrorData{"PerfLossLowClear", Entry::Level::Informational}}}}}}};
87 
88 ThresholdAlarmLogger::ThresholdAlarmLogger(
89     sdbusplus::bus_t& bus, sdeventplus::Event& event,
90     std::shared_ptr<PowerState> powerState) :
91     bus(bus),
92     event(event), _powerState(std::move(powerState)),
93     warningMatch(bus,
94                  "type='signal',member='PropertiesChanged',"
95                  "path_namespace='/xyz/openbmc_project/sensors',"
96                  "arg0='" +
97                      warningInterface + "'",
98                  std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
99                            std::placeholders::_1)),
100     criticalMatch(bus,
101                   "type='signal',member='PropertiesChanged',"
102                   "path_namespace='/xyz/openbmc_project/sensors',"
103                   "arg0='" +
104                       criticalInterface + "'",
105                   std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
106                             std::placeholders::_1)),
107     perfLossMatch(bus,
108                   "type='signal',member='PropertiesChanged',"
109                   "path_namespace='/xyz/openbmc_project/sensors',"
110                   "arg0='" +
111                       perfLossInterface + "'",
112                   std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
113                             std::placeholders::_1)),
114     ifacesRemovedMatch(bus,
115                        "type='signal',member='InterfacesRemoved',arg0path="
116                        "'/xyz/openbmc_project/sensors/'",
117                        std::bind(&ThresholdAlarmLogger::interfacesRemoved, this,
118                                  std::placeholders::_1)),
119     ifacesAddedMatch(bus,
120                      "type='signal',member='InterfacesAdded',arg0path="
121                      "'/xyz/openbmc_project/sensors/'",
122                      std::bind(&ThresholdAlarmLogger::interfacesAdded, this,
123                                std::placeholders::_1))
124 {
125     _powerState->addCallback("thresholdMon",
126                              std::bind(&ThresholdAlarmLogger::powerStateChanged,
127                                        this, std::placeholders::_1));
128 
129     // check for any currently asserted threshold alarms
130     std::for_each(thresholdData.begin(), thresholdData.end(),
131                   [this](const auto& thresholdInterface) {
132         const auto& interface = thresholdInterface.first;
133         auto objects = SDBusPlus::getSubTreeRaw(this->bus, "/", interface, 0);
134         std::for_each(objects.begin(), objects.end(),
135                       [interface, this](const auto& object) {
136             const auto& path = object.first;
137             const auto& service = object.second.begin()->first;
138             checkThresholds(interface, path, service);
139         });
140     });
141 }
142 
143 void ThresholdAlarmLogger::propertiesChanged(sdbusplus::message_t& msg)
144 {
145     std::map<std::string, std::variant<bool>> properties;
146     std::string sensorPath = msg.get_path();
147     std::string interface;
148 
149     msg.read(interface, properties);
150 
151     checkProperties(sensorPath, interface, properties);
152 }
153 
154 void ThresholdAlarmLogger::interfacesRemoved(sdbusplus::message_t& msg)
155 {
156     sdbusplus::message::object_path path;
157     std::vector<std::string> interfaces;
158 
159     msg.read(path, interfaces);
160 
161     for (const auto& interface : interfaces)
162     {
163         if (std::find(thresholdIfaceNames.begin(), thresholdIfaceNames.end(),
164                       interface) != thresholdIfaceNames.end())
165         {
166             alarms.erase(InterfaceKey{path, interface});
167         }
168     }
169 }
170 
171 void ThresholdAlarmLogger::interfacesAdded(sdbusplus::message_t& msg)
172 {
173     sdbusplus::message::object_path path;
174     std::map<std::string, std::map<std::string, std::variant<bool>>> interfaces;
175 
176     msg.read(path, interfaces);
177 
178     for (const auto& [interface, properties] : interfaces)
179     {
180         if (std::find(thresholdIfaceNames.begin(), thresholdIfaceNames.end(),
181                       interface) != thresholdIfaceNames.end())
182         {
183             checkProperties(path, interface, properties);
184         }
185     }
186 }
187 
188 void ThresholdAlarmLogger::checkProperties(
189     const std::string& sensorPath, const std::string& interface,
190     const std::map<std::string, std::variant<bool>>& properties)
191 {
192     auto alarmProperties = thresholdData.find(interface);
193     if (alarmProperties == thresholdData.end())
194     {
195         return;
196     }
197 
198     for (const auto& [propertyName, propertyValue] : properties)
199     {
200         if (alarmProperties->second.find(propertyName) !=
201             alarmProperties->second.end())
202         {
203             // If this is the first time we've seen this alarm, then
204             // assume it was off before so it doesn't create an event
205             // log for a value of false.
206 
207             InterfaceKey key{sensorPath, interface};
208             if (alarms.find(key) == alarms.end())
209             {
210                 alarms[key][propertyName] = false;
211             }
212 
213             // Check if the value changed from what was there before.
214             auto alarmValue = std::get<bool>(propertyValue);
215             if (alarmValue != alarms[key][propertyName])
216             {
217                 alarms[key][propertyName] = alarmValue;
218 
219                 if (_powerState->isPowerOn())
220                 {
221                     createEventLog(sensorPath, interface, propertyName,
222                                    alarmValue);
223                 }
224             }
225         }
226     }
227 }
228 
229 void ThresholdAlarmLogger::checkThresholds(const std::string& interface,
230                                            const std::string& sensorPath,
231                                            const std::string& service)
232 {
233     auto properties = thresholdData.find(interface);
234     if (properties == thresholdData.end())
235     {
236         return;
237     }
238 
239     for (const auto& [property, unused] : properties->second)
240     {
241         try
242         {
243             auto alarmValue = SDBusPlus::getProperty<bool>(
244                 bus, service, sensorPath, interface, property);
245             alarms[InterfaceKey(sensorPath, interface)][property] = alarmValue;
246 
247             // This is just for checking alarms on startup,
248             // so only look for active alarms.
249             if (alarmValue && _powerState->isPowerOn())
250             {
251                 createEventLog(sensorPath, interface, property, alarmValue);
252             }
253         }
254         catch (const sdbusplus::exception_t& e)
255         {
256             // Sensor daemons that get their direction from entity manager
257             // may only be putting either the high alarm or low alarm on
258             // D-Bus, not both.
259             continue;
260         }
261     }
262 }
263 
264 void ThresholdAlarmLogger::createEventLog(const std::string& sensorPath,
265                                           const std::string& interface,
266                                           const std::string& alarmProperty,
267                                           bool alarmValue)
268 {
269     std::map<std::string, std::string> ad;
270 
271     auto type = getSensorType(sensorPath);
272     if (skipSensorType(type))
273     {
274         return;
275     }
276 
277     auto it = thresholdData.find(interface);
278     if (it == thresholdData.end())
279     {
280         return;
281     }
282 
283     auto properties = it->second.find(alarmProperty);
284     if (properties == it->second.end())
285     {
286         log<level::INFO>(
287             fmt::format("Could not find {} in threshold alarms map",
288                         alarmProperty)
289                 .c_str());
290         return;
291     }
292 
293     ad.emplace("SENSOR_NAME", sensorPath);
294     ad.emplace("_PID", std::to_string(getpid()));
295 
296     try
297     {
298         auto sensorValue = SDBusPlus::getProperty<double>(
299             bus, sensorPath, valueInterface, "Value");
300 
301         ad.emplace("SENSOR_VALUE", std::to_string(sensorValue));
302 
303         log<level::INFO>(
304             fmt::format("Threshold Event {} {} = {} (sensor value {})",
305                         sensorPath, alarmProperty, alarmValue, sensorValue)
306                 .c_str());
307     }
308     catch (const DBusServiceError& e)
309     {
310         // If the sensor was just added, the Value interface for it may
311         // not be in the mapper yet.  This could only happen if the sensor
312         // application was started up after this one and the value exceeded the
313         // threshold immediately.
314         log<level::INFO>(fmt::format("Threshold Event {} {} = {}", sensorPath,
315                                      alarmProperty, alarmValue)
316                              .c_str());
317     }
318 
319     auto callout = getCallout(sensorPath);
320     if (!callout.empty())
321     {
322         ad.emplace("CALLOUT_INVENTORY_PATH", callout);
323     }
324 
325     auto errorData = properties->second.find(alarmValue);
326 
327     // Add the base error name and the sensor type (like Temperature) to the
328     // error name that's in the thresholdData name to get something like
329     // xyz.openbmc_project.Sensor.Threshold.Error.TemperatureWarningHigh
330     const auto& [name, severity] = errorData->second;
331     type.front() = toupper(type.front());
332     std::string errorName = errorNameBase + type + name;
333 
334     SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
335                           "Create", errorName, convertForMessage(severity), ad);
336 }
337 
338 std::string ThresholdAlarmLogger::getSensorType(std::string sensorPath)
339 {
340     auto pos = sensorPath.find_last_of('/');
341     if ((sensorPath.back() == '/') || (pos == std::string::npos))
342     {
343         log<level::ERR>(
344             fmt::format("Cannot get sensor type from sensor path {}",
345                         sensorPath)
346                 .c_str());
347         throw std::runtime_error("Invalid sensor path");
348     }
349 
350     sensorPath = sensorPath.substr(0, pos);
351     return sensorPath.substr(sensorPath.find_last_of('/') + 1);
352 }
353 
354 bool ThresholdAlarmLogger::skipSensorType(const std::string& type)
355 {
356     return (type == "utilization");
357 }
358 
359 std::string ThresholdAlarmLogger::getCallout(const std::string& sensorPath)
360 {
361     const std::array<std::string, 2> assocTypes{"inventory", "chassis"};
362 
363     // Different implementations handle the association to the FRU
364     // differently:
365     //  * phosphor-inventory-manager uses the 'inventory' association
366     //    to point to the FRU.
367     //  * dbus-sensors/entity-manager uses the 'chassis' association'.
368     //  * For virtual sensors, no association.
369 
370     for (const auto& assocType : assocTypes)
371     {
372         auto assocPath = sensorPath + "/" + assocType;
373 
374         try
375         {
376             auto endpoints = SDBusPlus::getProperty<std::vector<std::string>>(
377                 bus, assocPath, assocInterface, "endpoints");
378 
379             if (!endpoints.empty())
380             {
381                 return endpoints[0];
382             }
383         }
384         catch (const DBusServiceError& e)
385         {
386             // The association doesn't exist
387             continue;
388         }
389     }
390 
391     return std::string{};
392 }
393 
394 void ThresholdAlarmLogger::powerStateChanged(bool powerStateOn)
395 {
396     if (powerStateOn)
397     {
398         checkThresholds();
399     }
400 }
401 
402 void ThresholdAlarmLogger::checkThresholds()
403 {
404     std::vector<InterfaceKey> toErase;
405 
406     for (const auto& [interfaceKey, alarmMap] : alarms)
407     {
408         for (const auto& [propertyName, alarmValue] : alarmMap)
409         {
410             if (alarmValue)
411             {
412                 const auto& sensorPath = std::get<0>(interfaceKey);
413                 const auto& interface = std::get<1>(interfaceKey);
414                 std::string service;
415 
416                 try
417                 {
418                     // Check that the service that provides the alarm is still
419                     // running, because if it died when the alarm was active
420                     // there would be no indication of it unless we listened
421                     // for NameOwnerChanged and tracked services, and this is
422                     // easier.
423                     service = SDBusPlus::getService(bus, sensorPath, interface);
424                 }
425                 catch (const DBusServiceError& e)
426                 {
427                     // No longer on D-Bus delete the alarm entry
428                     toErase.emplace_back(sensorPath, interface);
429                 }
430 
431                 if (!service.empty())
432                 {
433                     createEventLog(sensorPath, interface, propertyName,
434                                    alarmValue);
435                 }
436             }
437         }
438     }
439 
440     for (const auto& e : toErase)
441     {
442         alarms.erase(e);
443     }
444 }
445 
446 } // namespace sensor::monitor
447