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::bus& 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 {
112     _powerState->addCallback("thresholdMon",
113                              std::bind(&ThresholdAlarmLogger::powerStateChanged,
114                                        this, std::placeholders::_1));
115 
116     // check for any currently asserted threshold alarms
117     std::for_each(
118         thresholdData.begin(), thresholdData.end(),
119         [this](const auto& thresholdInterface) {
120             const auto& interface = thresholdInterface.first;
121             auto objects =
122                 SDBusPlus::getSubTreeRaw(this->bus, "/", interface, 0);
123             std::for_each(objects.begin(), objects.end(),
124                           [interface, this](const auto& object) {
125                               const auto& path = object.first;
126                               const auto& service =
127                                   object.second.begin()->first;
128                               checkThresholds(interface, path, service);
129                           });
130         });
131 }
132 
133 void ThresholdAlarmLogger::propertiesChanged(sdbusplus::message::message& msg)
134 {
135     std::map<std::string, std::variant<bool>> properties;
136     std::string sensorPath = msg.get_path();
137     std::string interface;
138 
139     msg.read(interface, properties);
140 
141     auto alarmProperties = thresholdData.find(interface);
142     if (alarmProperties == thresholdData.end())
143     {
144         return;
145     }
146 
147     for (const auto& [propertyName, propertyValue] : properties)
148     {
149         if (alarmProperties->second.find(propertyName) !=
150             alarmProperties->second.end())
151         {
152             // If this is the first time we've seen this alarm, then
153             // assume it was off before so it doesn't create an event
154             // log for a value of false.
155 
156             InterfaceKey key{sensorPath, interface};
157             if (alarms.find(key) == alarms.end())
158             {
159                 alarms[key][propertyName] = false;
160             }
161 
162             // Check if the value changed from what was there before.
163             auto alarmValue = std::get<bool>(propertyValue);
164             if (alarmValue != alarms[key][propertyName])
165             {
166                 alarms[key][propertyName] = alarmValue;
167 
168                 if (_powerState->isPowerOn())
169                 {
170                     createEventLog(sensorPath, interface, propertyName,
171                                    alarmValue);
172                 }
173             }
174         }
175     }
176 }
177 
178 void ThresholdAlarmLogger::checkThresholds(const std::string& interface,
179                                            const std::string& sensorPath,
180                                            const std::string& service)
181 {
182     auto properties = thresholdData.find(interface);
183     if (properties == thresholdData.end())
184     {
185         return;
186     }
187 
188     for (const auto& [property, unused] : properties->second)
189     {
190         try
191         {
192             auto alarmValue = SDBusPlus::getProperty<bool>(
193                 bus, service, sensorPath, interface, property);
194             alarms[InterfaceKey(sensorPath, interface)][property] = alarmValue;
195 
196             // This is just for checking alarms on startup,
197             // so only look for active alarms.
198             if (alarmValue && _powerState->isPowerOn())
199             {
200                 createEventLog(sensorPath, interface, property, alarmValue);
201             }
202         }
203         catch (const DBusError& e)
204         {
205             log<level::ERR>(
206                 fmt::format("Failed reading sensor threshold properties: {}",
207                             e.what())
208                     .c_str());
209             continue;
210         }
211     }
212 }
213 
214 void ThresholdAlarmLogger::createEventLog(const std::string& sensorPath,
215                                           const std::string& interface,
216                                           const std::string& alarmProperty,
217                                           bool alarmValue)
218 {
219     std::map<std::string, std::string> ad;
220 
221     auto type = getSensorType(sensorPath);
222     if (skipSensorType(type))
223     {
224         return;
225     }
226 
227     auto it = thresholdData.find(interface);
228     if (it == thresholdData.end())
229     {
230         return;
231     }
232 
233     auto properties = it->second.find(alarmProperty);
234     if (properties == it->second.end())
235     {
236         log<level::INFO>(
237             fmt::format("Could not find {} in threshold alarms map",
238                         alarmProperty)
239                 .c_str());
240         return;
241     }
242 
243     ad.emplace("SENSOR_NAME", sensorPath);
244     ad.emplace("_PID", std::to_string(getpid()));
245 
246     try
247     {
248         auto sensorValue = SDBusPlus::getProperty<double>(
249             bus, sensorPath, valueInterface, "Value");
250 
251         ad.emplace("SENSOR_VALUE", std::to_string(sensorValue));
252 
253         log<level::INFO>(
254             fmt::format("Threshold Event {} {} = {} (sensor value {})",
255                         sensorPath, alarmProperty, alarmValue, sensorValue)
256                 .c_str());
257     }
258     catch (const DBusServiceError& e)
259     {
260         // If the sensor was just added, the Value interface for it may
261         // not be in the mapper yet.  This could only happen if the sensor
262         // application was started up after this one and the value exceeded the
263         // threshold immediately.
264         log<level::INFO>(fmt::format("Threshold Event {} {} = {}", sensorPath,
265                                      alarmProperty, alarmValue)
266                              .c_str());
267     }
268 
269     auto callout = getCallout(sensorPath);
270     if (!callout.empty())
271     {
272         ad.emplace("CALLOUT_INVENTORY_PATH", callout);
273     }
274 
275     auto errorData = properties->second.find(alarmValue);
276 
277     // Add the base error name and the sensor type (like Temperature) to the
278     // error name that's in the thresholdData name to get something like
279     // xyz.openbmc_project.Sensor.Threshold.Error.TemperatureWarningHigh
280     const auto& [name, severity] = errorData->second;
281     type.front() = toupper(type.front());
282     std::string errorName = errorNameBase + type + name;
283 
284     SDBusPlus::callMethod(loggingService, loggingPath, loggingCreateIface,
285                           "Create", errorName, convertForMessage(severity), ad);
286 }
287 
288 std::string ThresholdAlarmLogger::getSensorType(std::string sensorPath)
289 {
290     auto pos = sensorPath.find_last_of('/');
291     if ((sensorPath.back() == '/') || (pos == std::string::npos))
292     {
293         log<level::ERR>(
294             fmt::format("Cannot get sensor type from sensor path {}",
295                         sensorPath)
296                 .c_str());
297         throw std::runtime_error("Invalid sensor path");
298     }
299 
300     sensorPath = sensorPath.substr(0, pos);
301     return sensorPath.substr(sensorPath.find_last_of('/') + 1);
302 }
303 
304 bool ThresholdAlarmLogger::skipSensorType(const std::string& type)
305 {
306     return (type == "utilization");
307 }
308 
309 std::string ThresholdAlarmLogger::getCallout(const std::string& sensorPath)
310 {
311     const std::array<std::string, 2> assocTypes{"inventory", "chassis"};
312 
313     // Different implementations handle the association to the FRU
314     // differently:
315     //  * phosphor-inventory-manager uses the 'inventory' association
316     //    to point to the FRU.
317     //  * dbus-sensors/entity-manager uses the 'chassis' association'.
318     //  * For virtual sensors, no association.
319 
320     for (const auto& assocType : assocTypes)
321     {
322         auto assocPath = sensorPath + "/" + assocType;
323 
324         try
325         {
326             auto endpoints = SDBusPlus::getProperty<std::vector<std::string>>(
327                 bus, assocPath, assocInterface, "endpoints");
328 
329             if (!endpoints.empty())
330             {
331                 return endpoints[0];
332             }
333         }
334         catch (const DBusServiceError& e)
335         {
336             // The association doesn't exist
337             continue;
338         }
339     }
340 
341     return std::string{};
342 }
343 
344 void ThresholdAlarmLogger::powerStateChanged(bool powerStateOn)
345 {
346     if (powerStateOn)
347     {
348         checkThresholds();
349     }
350 }
351 
352 void ThresholdAlarmLogger::checkThresholds()
353 {
354     for (const auto& [interfaceKey, alarmMap] : alarms)
355     {
356         for (const auto& [propertyName, alarmValue] : alarmMap)
357         {
358             if (alarmValue)
359             {
360                 const auto& sensorPath = std::get<0>(interfaceKey);
361                 const auto& interface = std::get<1>(interfaceKey);
362 
363                 createEventLog(sensorPath, interface, propertyName, alarmValue);
364             }
365         }
366     }
367 }
368 
369 } // namespace sensor::monitor
370