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(
131         thresholdData.begin(), thresholdData.end(),
132         [this](const auto& thresholdInterface) {
133             const auto& interface = thresholdInterface.first;
134             auto objects =
135                 SDBusPlus::getSubTreeRaw(this->bus, "/", interface, 0);
136             std::for_each(objects.begin(), objects.end(),
137                           [interface, this](const auto& object) {
138                               const auto& path = object.first;
139                               const auto& service =
140                                   object.second.begin()->first;
141                               checkThresholds(interface, path, service);
142                           });
143         });
144 }
145 
146 void ThresholdAlarmLogger::propertiesChanged(sdbusplus::message_t& msg)
147 {
148     std::map<std::string, std::variant<bool>> properties;
149     std::string sensorPath = msg.get_path();
150     std::string interface;
151 
152     msg.read(interface, properties);
153 
154     checkProperties(sensorPath, interface, properties);
155 }
156 
157 void ThresholdAlarmLogger::interfacesRemoved(sdbusplus::message_t& msg)
158 {
159     sdbusplus::message::object_path path;
160     std::vector<std::string> interfaces;
161 
162     msg.read(path, interfaces);
163 
164     for (const auto& interface : interfaces)
165     {
166         if (std::find(thresholdIfaceNames.begin(), thresholdIfaceNames.end(),
167                       interface) != thresholdIfaceNames.end())
168         {
169             alarms.erase(InterfaceKey{path, interface});
170         }
171     }
172 }
173 
174 void ThresholdAlarmLogger::interfacesAdded(sdbusplus::message_t& msg)
175 {
176     sdbusplus::message::object_path path;
177     std::map<std::string, std::map<std::string, std::variant<bool>>> interfaces;
178 
179     msg.read(path, interfaces);
180 
181     for (const auto& [interface, properties] : interfaces)
182     {
183         if (std::find(thresholdIfaceNames.begin(), thresholdIfaceNames.end(),
184                       interface) != thresholdIfaceNames.end())
185         {
186             checkProperties(path, interface, properties);
187         }
188     }
189 }
190 
191 void ThresholdAlarmLogger::checkProperties(
192     const std::string& sensorPath, const std::string& interface,
193     const std::map<std::string, std::variant<bool>>& properties)
194 {
195     auto alarmProperties = thresholdData.find(interface);
196     if (alarmProperties == thresholdData.end())
197     {
198         return;
199     }
200 
201     for (const auto& [propertyName, propertyValue] : properties)
202     {
203         if (alarmProperties->second.find(propertyName) !=
204             alarmProperties->second.end())
205         {
206             // If this is the first time we've seen this alarm, then
207             // assume it was off before so it doesn't create an event
208             // log for a value of false.
209 
210             InterfaceKey key{sensorPath, interface};
211             if (alarms.find(key) == alarms.end())
212             {
213                 alarms[key][propertyName] = false;
214             }
215 
216             // Check if the value changed from what was there before.
217             auto alarmValue = std::get<bool>(propertyValue);
218             if (alarmValue != alarms[key][propertyName])
219             {
220                 alarms[key][propertyName] = alarmValue;
221 
222                 if (_powerState->isPowerOn())
223                 {
224                     createEventLog(sensorPath, interface, propertyName,
225                                    alarmValue);
226                 }
227             }
228         }
229     }
230 }
231 
232 void ThresholdAlarmLogger::checkThresholds(const std::string& interface,
233                                            const std::string& sensorPath,
234                                            const std::string& service)
235 {
236     auto properties = thresholdData.find(interface);
237     if (properties == thresholdData.end())
238     {
239         return;
240     }
241 
242     for (const auto& [property, unused] : properties->second)
243     {
244         try
245         {
246             auto alarmValue = SDBusPlus::getProperty<bool>(
247                 bus, service, sensorPath, interface, property);
248             alarms[InterfaceKey(sensorPath, interface)][property] = alarmValue;
249 
250             // This is just for checking alarms on startup,
251             // so only look for active alarms.
252             if (alarmValue && _powerState->isPowerOn())
253             {
254                 createEventLog(sensorPath, interface, property, alarmValue);
255             }
256         }
257         catch (const sdbusplus::exception_t& e)
258         {
259             // Sensor daemons that get their direction from entity manager
260             // may only be putting either the high alarm or low alarm on
261             // D-Bus, not both.
262             continue;
263         }
264     }
265 }
266 
267 void ThresholdAlarmLogger::createEventLog(const std::string& sensorPath,
268                                           const std::string& interface,
269                                           const std::string& alarmProperty,
270                                           bool alarmValue)
271 {
272     std::map<std::string, std::string> ad;
273 
274     auto type = getSensorType(sensorPath);
275     if (skipSensorType(type))
276     {
277         return;
278     }
279 
280     auto it = thresholdData.find(interface);
281     if (it == thresholdData.end())
282     {
283         return;
284     }
285 
286     auto properties = it->second.find(alarmProperty);
287     if (properties == it->second.end())
288     {
289         log<level::INFO>(
290             fmt::format("Could not find {} in threshold alarms map",
291                         alarmProperty)
292                 .c_str());
293         return;
294     }
295 
296     ad.emplace("SENSOR_NAME", sensorPath);
297     ad.emplace("_PID", std::to_string(getpid()));
298 
299     try
300     {
301         auto sensorValue = SDBusPlus::getProperty<double>(
302             bus, sensorPath, valueInterface, "Value");
303 
304         ad.emplace("SENSOR_VALUE", std::to_string(sensorValue));
305 
306         log<level::INFO>(
307             fmt::format("Threshold Event {} {} = {} (sensor value {})",
308                         sensorPath, alarmProperty, alarmValue, sensorValue)
309                 .c_str());
310     }
311     catch (const DBusServiceError& e)
312     {
313         // If the sensor was just added, the Value interface for it may
314         // not be in the mapper yet.  This could only happen if the sensor
315         // application was started up after this one and the value exceeded the
316         // threshold immediately.
317         log<level::INFO>(fmt::format("Threshold Event {} {} = {}", sensorPath,
318                                      alarmProperty, alarmValue)
319                              .c_str());
320     }
321 
322     auto callout = getCallout(sensorPath);
323     if (!callout.empty())
324     {
325         ad.emplace("CALLOUT_INVENTORY_PATH", callout);
326     }
327 
328     auto errorData = properties->second.find(alarmValue);
329 
330     // Add the base error name and the sensor type (like Temperature) to the
331     // error name that's in the thresholdData name to get something like
332     // xyz.openbmc_project.Sensor.Threshold.Error.TemperatureWarningHigh
333     const auto& [name, severity] = errorData->second;
334     type.front() = toupper(type.front());
335     std::string errorName = errorNameBase + type + name;
336 
337     SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
338                           "Create", errorName, convertForMessage(severity), ad);
339 }
340 
341 std::string ThresholdAlarmLogger::getSensorType(std::string sensorPath)
342 {
343     auto pos = sensorPath.find_last_of('/');
344     if ((sensorPath.back() == '/') || (pos == std::string::npos))
345     {
346         log<level::ERR>(
347             fmt::format("Cannot get sensor type from sensor path {}",
348                         sensorPath)
349                 .c_str());
350         throw std::runtime_error("Invalid sensor path");
351     }
352 
353     sensorPath = sensorPath.substr(0, pos);
354     return sensorPath.substr(sensorPath.find_last_of('/') + 1);
355 }
356 
357 bool ThresholdAlarmLogger::skipSensorType(const std::string& type)
358 {
359     return (type == "utilization");
360 }
361 
362 std::string ThresholdAlarmLogger::getCallout(const std::string& sensorPath)
363 {
364     const std::array<std::string, 2> assocTypes{"inventory", "chassis"};
365 
366     // Different implementations handle the association to the FRU
367     // differently:
368     //  * phosphor-inventory-manager uses the 'inventory' association
369     //    to point to the FRU.
370     //  * dbus-sensors/entity-manager uses the 'chassis' association'.
371     //  * For virtual sensors, no association.
372 
373     for (const auto& assocType : assocTypes)
374     {
375         auto assocPath = sensorPath + "/" + assocType;
376 
377         try
378         {
379             auto endpoints = SDBusPlus::getProperty<std::vector<std::string>>(
380                 bus, assocPath, assocInterface, "endpoints");
381 
382             if (!endpoints.empty())
383             {
384                 return endpoints[0];
385             }
386         }
387         catch (const DBusServiceError& e)
388         {
389             // The association doesn't exist
390             continue;
391         }
392     }
393 
394     return std::string{};
395 }
396 
397 void ThresholdAlarmLogger::powerStateChanged(bool powerStateOn)
398 {
399     if (powerStateOn)
400     {
401         checkThresholds();
402     }
403 }
404 
405 void ThresholdAlarmLogger::checkThresholds()
406 {
407     std::vector<InterfaceKey> toErase;
408 
409     for (const auto& [interfaceKey, alarmMap] : alarms)
410     {
411         for (const auto& [propertyName, alarmValue] : alarmMap)
412         {
413             if (alarmValue)
414             {
415                 const auto& sensorPath = std::get<0>(interfaceKey);
416                 const auto& interface = std::get<1>(interfaceKey);
417                 std::string service;
418 
419                 try
420                 {
421                     // Check that the service that provides the alarm is still
422                     // running, because if it died when the alarm was active
423                     // there would be no indication of it unless we listened
424                     // for NameOwnerChanged and tracked services, and this is
425                     // easier.
426                     service = SDBusPlus::getService(bus, sensorPath, interface);
427                 }
428                 catch (const DBusServiceError& e)
429                 {
430                     // No longer on D-Bus delete the alarm entry
431                     toErase.emplace_back(sensorPath, interface);
432                 }
433 
434                 if (!service.empty())
435                 {
436                     createEventLog(sensorPath, interface, propertyName,
437                                    alarmValue);
438                 }
439             }
440         }
441     }
442 
443     for (const auto& e : toErase)
444     {
445         alarms.erase(e);
446     }
447 }
448 
449 } // namespace sensor::monitor
450