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 using ErrorData = std::tuple<ErrorName, Entry::Level>;
48 
49 /**
50  * Map of threshold interfaces and alarm properties and values to error data.
51  */
52 const std::map<InterfaceName, std::map<PropertyName, std::map<bool, ErrorData>>>
53     thresholdData{
54 
55         {warningInterface,
56          {{"WarningAlarmHigh",
57            {{true, ErrorData{"WarningHigh", Entry::Level::Warning}},
58             {false,
59              ErrorData{"WarningHighClear", Entry::Level::Informational}}}},
60           {"WarningAlarmLow",
61            {{true, ErrorData{"WarningLow", Entry::Level::Warning}},
62             {false,
63              ErrorData{"WarningLowClear", Entry::Level::Informational}}}}}},
64 
65         {criticalInterface,
66          {{"CriticalAlarmHigh",
67            {{true, ErrorData{"CriticalHigh", Entry::Level::Critical}},
68             {false,
69              ErrorData{"CriticalHighClear", Entry::Level::Informational}}}},
70           {"CriticalAlarmLow",
71            {{true, ErrorData{"CriticalLow", Entry::Level::Critical}},
72             {false,
73              ErrorData{"CriticalLowClear", Entry::Level::Informational}}}}}},
74 
75         {perfLossInterface,
76          {{"PerfLossAlarmHigh",
77            {{true, ErrorData{"PerfLossHigh", Entry::Level::Warning}},
78             {false,
79              ErrorData{"PerfLossHighClear", Entry::Level::Informational}}}},
80           {"PerfLossAlarmLow",
81            {{true, ErrorData{"PerfLossLow", Entry::Level::Warning}},
82             {false,
83              ErrorData{"PerfLossLowClear", Entry::Level::Informational}}}}}}};
84 
85 ThresholdAlarmLogger::ThresholdAlarmLogger(
86     sdbusplus::bus_t& bus, sdeventplus::Event& event,
87     std::shared_ptr<PowerState> powerState) :
88     bus(bus),
89     event(event), _powerState(std::move(powerState)),
90     warningMatch(bus,
91                  "type='signal',member='PropertiesChanged',"
92                  "path_namespace='/xyz/openbmc_project/sensors',"
93                  "arg0='" +
94                      warningInterface + "'",
95                  std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
96                            std::placeholders::_1)),
97     criticalMatch(bus,
98                   "type='signal',member='PropertiesChanged',"
99                   "path_namespace='/xyz/openbmc_project/sensors',"
100                   "arg0='" +
101                       criticalInterface + "'",
102                   std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
103                             std::placeholders::_1)),
104     perfLossMatch(bus,
105                   "type='signal',member='PropertiesChanged',"
106                   "path_namespace='/xyz/openbmc_project/sensors',"
107                   "arg0='" +
108                       perfLossInterface + "'",
109                   std::bind(&ThresholdAlarmLogger::propertiesChanged, this,
110                             std::placeholders::_1)),
111     ifacesRemovedMatch(bus,
112                        "type='signal',member='InterfacesRemoved',arg0path="
113                        "'/xyz/openbmc_project/sensors/'",
114                        std::bind(&ThresholdAlarmLogger::interfacesRemoved, this,
115                                  std::placeholders::_1))
116 {
117     _powerState->addCallback("thresholdMon",
118                              std::bind(&ThresholdAlarmLogger::powerStateChanged,
119                                        this, std::placeholders::_1));
120 
121     // check for any currently asserted threshold alarms
122     std::for_each(
123         thresholdData.begin(), thresholdData.end(),
124         [this](const auto& thresholdInterface) {
125             const auto& interface = thresholdInterface.first;
126             auto objects =
127                 SDBusPlus::getSubTreeRaw(this->bus, "/", interface, 0);
128             std::for_each(objects.begin(), objects.end(),
129                           [interface, this](const auto& object) {
130                               const auto& path = object.first;
131                               const auto& service =
132                                   object.second.begin()->first;
133                               checkThresholds(interface, path, service);
134                           });
135         });
136 }
137 
138 void ThresholdAlarmLogger::propertiesChanged(sdbusplus::message_t& msg)
139 {
140     std::map<std::string, std::variant<bool>> properties;
141     std::string sensorPath = msg.get_path();
142     std::string interface;
143 
144     msg.read(interface, properties);
145 
146     auto alarmProperties = thresholdData.find(interface);
147     if (alarmProperties == thresholdData.end())
148     {
149         return;
150     }
151 
152     for (const auto& [propertyName, propertyValue] : properties)
153     {
154         if (alarmProperties->second.find(propertyName) !=
155             alarmProperties->second.end())
156         {
157             // If this is the first time we've seen this alarm, then
158             // assume it was off before so it doesn't create an event
159             // log for a value of false.
160 
161             InterfaceKey key{sensorPath, interface};
162             if (alarms.find(key) == alarms.end())
163             {
164                 alarms[key][propertyName] = false;
165             }
166 
167             // Check if the value changed from what was there before.
168             auto alarmValue = std::get<bool>(propertyValue);
169             if (alarmValue != alarms[key][propertyName])
170             {
171                 alarms[key][propertyName] = alarmValue;
172 
173                 if (_powerState->isPowerOn())
174                 {
175                     createEventLog(sensorPath, interface, propertyName,
176                                    alarmValue);
177                 }
178             }
179         }
180     }
181 }
182 
183 void ThresholdAlarmLogger::interfacesRemoved(sdbusplus::message_t& msg)
184 {
185     static const std::vector<std::string> thresholdNames{
186         warningInterface, criticalInterface, perfLossInterface};
187     sdbusplus::message::object_path path;
188     std::vector<std::string> interfaces;
189 
190     msg.read(path, interfaces);
191 
192     for (const auto& interface : interfaces)
193     {
194         if (std::find(thresholdNames.begin(), thresholdNames.end(),
195                       interface) != thresholdNames.end())
196         {
197             alarms.erase(InterfaceKey{path, interface});
198         }
199     }
200 }
201 
202 void ThresholdAlarmLogger::checkThresholds(const std::string& interface,
203                                            const std::string& sensorPath,
204                                            const std::string& service)
205 {
206     auto properties = thresholdData.find(interface);
207     if (properties == thresholdData.end())
208     {
209         return;
210     }
211 
212     for (const auto& [property, unused] : properties->second)
213     {
214         try
215         {
216             auto alarmValue = SDBusPlus::getProperty<bool>(
217                 bus, service, sensorPath, interface, property);
218             alarms[InterfaceKey(sensorPath, interface)][property] = alarmValue;
219 
220             // This is just for checking alarms on startup,
221             // so only look for active alarms.
222             if (alarmValue && _powerState->isPowerOn())
223             {
224                 createEventLog(sensorPath, interface, property, alarmValue);
225             }
226         }
227         catch (const sdbusplus::exception_t& e)
228         {
229             // Sensor daemons that get their direction from entity manager
230             // may only be putting either the high alarm or low alarm on
231             // D-Bus, not both.
232             continue;
233         }
234     }
235 }
236 
237 void ThresholdAlarmLogger::createEventLog(const std::string& sensorPath,
238                                           const std::string& interface,
239                                           const std::string& alarmProperty,
240                                           bool alarmValue)
241 {
242     std::map<std::string, std::string> ad;
243 
244     auto type = getSensorType(sensorPath);
245     if (skipSensorType(type))
246     {
247         return;
248     }
249 
250     auto it = thresholdData.find(interface);
251     if (it == thresholdData.end())
252     {
253         return;
254     }
255 
256     auto properties = it->second.find(alarmProperty);
257     if (properties == it->second.end())
258     {
259         log<level::INFO>(
260             fmt::format("Could not find {} in threshold alarms map",
261                         alarmProperty)
262                 .c_str());
263         return;
264     }
265 
266     ad.emplace("SENSOR_NAME", sensorPath);
267     ad.emplace("_PID", std::to_string(getpid()));
268 
269     try
270     {
271         auto sensorValue = SDBusPlus::getProperty<double>(
272             bus, sensorPath, valueInterface, "Value");
273 
274         ad.emplace("SENSOR_VALUE", std::to_string(sensorValue));
275 
276         log<level::INFO>(
277             fmt::format("Threshold Event {} {} = {} (sensor value {})",
278                         sensorPath, alarmProperty, alarmValue, sensorValue)
279                 .c_str());
280     }
281     catch (const DBusServiceError& e)
282     {
283         // If the sensor was just added, the Value interface for it may
284         // not be in the mapper yet.  This could only happen if the sensor
285         // application was started up after this one and the value exceeded the
286         // threshold immediately.
287         log<level::INFO>(fmt::format("Threshold Event {} {} = {}", sensorPath,
288                                      alarmProperty, alarmValue)
289                              .c_str());
290     }
291 
292     auto callout = getCallout(sensorPath);
293     if (!callout.empty())
294     {
295         ad.emplace("CALLOUT_INVENTORY_PATH", callout);
296     }
297 
298     auto errorData = properties->second.find(alarmValue);
299 
300     // Add the base error name and the sensor type (like Temperature) to the
301     // error name that's in the thresholdData name to get something like
302     // xyz.openbmc_project.Sensor.Threshold.Error.TemperatureWarningHigh
303     const auto& [name, severity] = errorData->second;
304     type.front() = toupper(type.front());
305     std::string errorName = errorNameBase + type + name;
306 
307     SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
308                           "Create", errorName, convertForMessage(severity), ad);
309 }
310 
311 std::string ThresholdAlarmLogger::getSensorType(std::string sensorPath)
312 {
313     auto pos = sensorPath.find_last_of('/');
314     if ((sensorPath.back() == '/') || (pos == std::string::npos))
315     {
316         log<level::ERR>(
317             fmt::format("Cannot get sensor type from sensor path {}",
318                         sensorPath)
319                 .c_str());
320         throw std::runtime_error("Invalid sensor path");
321     }
322 
323     sensorPath = sensorPath.substr(0, pos);
324     return sensorPath.substr(sensorPath.find_last_of('/') + 1);
325 }
326 
327 bool ThresholdAlarmLogger::skipSensorType(const std::string& type)
328 {
329     return (type == "utilization");
330 }
331 
332 std::string ThresholdAlarmLogger::getCallout(const std::string& sensorPath)
333 {
334     const std::array<std::string, 2> assocTypes{"inventory", "chassis"};
335 
336     // Different implementations handle the association to the FRU
337     // differently:
338     //  * phosphor-inventory-manager uses the 'inventory' association
339     //    to point to the FRU.
340     //  * dbus-sensors/entity-manager uses the 'chassis' association'.
341     //  * For virtual sensors, no association.
342 
343     for (const auto& assocType : assocTypes)
344     {
345         auto assocPath = sensorPath + "/" + assocType;
346 
347         try
348         {
349             auto endpoints = SDBusPlus::getProperty<std::vector<std::string>>(
350                 bus, assocPath, assocInterface, "endpoints");
351 
352             if (!endpoints.empty())
353             {
354                 return endpoints[0];
355             }
356         }
357         catch (const DBusServiceError& e)
358         {
359             // The association doesn't exist
360             continue;
361         }
362     }
363 
364     return std::string{};
365 }
366 
367 void ThresholdAlarmLogger::powerStateChanged(bool powerStateOn)
368 {
369     if (powerStateOn)
370     {
371         checkThresholds();
372     }
373 }
374 
375 void ThresholdAlarmLogger::checkThresholds()
376 {
377     for (const auto& [interfaceKey, alarmMap] : alarms)
378     {
379         for (const auto& [propertyName, alarmValue] : alarmMap)
380         {
381             if (alarmValue)
382             {
383                 const auto& sensorPath = std::get<0>(interfaceKey);
384                 const auto& interface = std::get<1>(interfaceKey);
385 
386                 createEventLog(sensorPath, interface, propertyName, alarmValue);
387             }
388         }
389     }
390 }
391 
392 } // namespace sensor::monitor
393