xref: /openbmc/dbus-sensors/src/external/ExternalSensorMain.cpp (revision 89be6147e5a7ffa86d88d8f3e27eba8eb2c3a9da)
1 #include "ExternalSensor.hpp"
2 #include "Thresholds.hpp"
3 #include "Utils.hpp"
4 #include "VariantVisitors.hpp"
5 
6 #include <boost/asio/error.hpp>
7 #include <boost/asio/io_context.hpp>
8 #include <boost/asio/post.hpp>
9 #include <boost/asio/steady_timer.hpp>
10 #include <boost/container/flat_map.hpp>
11 #include <boost/container/flat_set.hpp>
12 #include <phosphor-logging/lg2.hpp>
13 #include <sdbusplus/asio/connection.hpp>
14 #include <sdbusplus/asio/object_server.hpp>
15 #include <sdbusplus/bus/match.hpp>
16 #include <sdbusplus/message.hpp>
17 #include <sdbusplus/message/native_types.hpp>
18 
19 #include <algorithm>
20 #include <array>
21 #include <chrono>
22 #include <cmath>
23 #include <functional>
24 #include <memory>
25 #include <string>
26 #include <utility>
27 #include <variant>
28 #include <vector>
29 
30 // Copied from HwmonTempSensor and inspired by
31 // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476
32 
33 // The ExternalSensor is a sensor whose value is intended to be writable
34 // by something external to the BMC, so that the host (or something else)
35 // can write to it, perhaps by using an IPMI or Redfish connection.
36 
37 // Unlike most other sensors, an external sensor does not correspond
38 // to a hwmon file or any other kernel/hardware interface,
39 // so, after initialization, this module does not have much to do,
40 // but it handles reinitialization and thresholds, similar to the others.
41 // The main work of this module is to provide backing storage for a
42 // sensor that exists only virtually, and to provide an optional
43 // timeout service for detecting loss of timely updates.
44 
45 // As there is no corresponding driver or hardware to support,
46 // all configuration of this sensor comes from the JSON parameters:
47 // MinValue, MaxValue, Timeout, PowerState, Units, Name
48 
49 // The purpose of "Units" is to specify the physical characteristic
50 // the external sensor is measuring, because with an external sensor
51 // there is no other way to tell, and it will be used for the object path
52 // here: /xyz/openbmc_project/sensors/<Units>/<Name>
53 
54 // For more information, see external-sensor.md design document:
55 // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452
56 // https://github.com/openbmc/docs/tree/master/designs/
57 
58 static const char* sensorType = "ExternalSensor";
59 
updateReaper(boost::container::flat_map<std::string,std::shared_ptr<ExternalSensor>> & sensors,boost::asio::steady_timer & timer,const std::chrono::steady_clock::time_point & now)60 void updateReaper(
61     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
62         sensors,
63     boost::asio::steady_timer& timer,
64     const std::chrono::steady_clock::time_point& now)
65 {
66     // First pass, reap all stale sensors
67     for (const auto& [name, sensor] : sensors)
68     {
69         if (!sensor)
70         {
71             continue;
72         }
73 
74         if (!sensor->isAliveAndPerishable())
75         {
76             continue;
77         }
78 
79         if (!sensor->isAliveAndFresh(now))
80         {
81             // Mark sensor as dead, no longer alive
82             sensor->writeInvalidate();
83         }
84     }
85 
86     std::chrono::steady_clock::duration nextCheck;
87     bool needCheck = false;
88 
89     // Second pass, determine timer interval to next check
90     for (const auto& [name, sensor] : sensors)
91     {
92         if (!sensor)
93         {
94             continue;
95         }
96 
97         if (!sensor->isAliveAndPerishable())
98         {
99             continue;
100         }
101 
102         auto expiration = sensor->ageRemaining(now);
103 
104         if (needCheck)
105         {
106             nextCheck = std::min(nextCheck, expiration);
107         }
108         else
109         {
110             // Initialization
111             nextCheck = expiration;
112             needCheck = true;
113         }
114     }
115 
116     if (!needCheck)
117     {
118         lg2::debug("Next ExternalSensor timer idle");
119 
120         return;
121     }
122 
123     timer.expires_at(now + nextCheck);
124 
125     timer.async_wait([&sensors, &timer](const boost::system::error_code& err) {
126         if (err != boost::system::errc::success)
127         {
128             // Cancellation is normal, as timer is dynamically rescheduled
129             if (err != boost::asio::error::operation_aborted)
130             {
131                 lg2::error(
132                     "ExternalSensor timer scheduling problem: {ERROR_MESSAGE}",
133                     "ERROR_MESSAGE", err.message());
134             }
135             return;
136         }
137 
138         updateReaper(sensors, timer, std::chrono::steady_clock::now());
139     });
140 
141     lg2::debug("Next ExternalSensor timer '{VALUE}' us", "VALUE",
142                std::chrono::duration_cast<std::chrono::microseconds>(nextCheck)
143                    .count());
144 }
145 
createSensors(sdbusplus::asio::object_server & objectServer,boost::container::flat_map<std::string,std::shared_ptr<ExternalSensor>> & sensors,std::shared_ptr<sdbusplus::asio::connection> & dbusConnection,const std::shared_ptr<boost::container::flat_set<std::string>> & sensorsChanged,boost::asio::steady_timer & reaperTimer)146 void createSensors(
147     sdbusplus::asio::object_server& objectServer,
148     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
149         sensors,
150     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
151     const std::shared_ptr<boost::container::flat_set<std::string>>&
152         sensorsChanged,
153     boost::asio::steady_timer& reaperTimer)
154 {
155     lg2::debug("ExternalSensor considering creating sensors");
156 
157     auto getter = std::make_shared<GetSensorConfiguration>(
158         dbusConnection,
159         [&objectServer, &sensors, &dbusConnection, sensorsChanged,
160          &reaperTimer](const ManagedObjectType& sensorConfigurations) {
161             bool firstScan = (sensorsChanged == nullptr);
162 
163             for (const std::pair<sdbusplus::message::object_path, SensorData>&
164                      sensor : sensorConfigurations)
165             {
166                 const std::string& interfacePath = sensor.first.str;
167                 const SensorData& sensorData = sensor.second;
168 
169                 auto sensorBase =
170                     sensorData.find(configInterfaceName(sensorType));
171                 if (sensorBase == sensorData.end())
172                 {
173                     lg2::error("Base configuration not found for '{PATH}'",
174                                "PATH", interfacePath);
175                     continue;
176                 }
177 
178                 const SensorBaseConfiguration& baseConfiguration = *sensorBase;
179                 const SensorBaseConfigMap& baseConfigMap =
180                     baseConfiguration.second;
181 
182                 // MinValue and MinValue are mandatory numeric parameters
183                 auto minFound = baseConfigMap.find("MinValue");
184                 if (minFound == baseConfigMap.end())
185                 {
186                     lg2::error("MinValue parameter not found for '{PATH}'",
187                                "PATH", interfacePath);
188                     continue;
189                 }
190                 double minValue =
191                     std::visit(VariantToDoubleVisitor(), minFound->second);
192                 if (!std::isfinite(minValue))
193                 {
194                     lg2::error("MinValue parameter not parsed for '{PATH}'",
195                                "PATH", interfacePath);
196                     continue;
197                 }
198 
199                 auto maxFound = baseConfigMap.find("MaxValue");
200                 if (maxFound == baseConfigMap.end())
201                 {
202                     lg2::error("MaxValue parameter not found for '{PATH}'",
203                                "PATH", interfacePath);
204                     continue;
205                 }
206                 double maxValue =
207                     std::visit(VariantToDoubleVisitor(), maxFound->second);
208                 if (!std::isfinite(maxValue))
209                 {
210                     lg2::error("MaxValue parameter not parsed for '{PATH}'",
211                                "PATH", interfacePath);
212                     continue;
213                 }
214 
215                 double timeoutSecs = 0.0;
216 
217                 // Timeout is an optional numeric parameter
218                 auto timeoutFound = baseConfigMap.find("Timeout");
219                 if (timeoutFound != baseConfigMap.end())
220                 {
221                     timeoutSecs = std::visit(VariantToDoubleVisitor(),
222                                              timeoutFound->second);
223                 }
224                 if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0))
225                 {
226                     lg2::error("Timeout parameter not parsed for '{PATH}'",
227                                "PATH", interfacePath);
228                     continue;
229                 }
230 
231                 std::string sensorName;
232                 std::string sensorUnits;
233 
234                 // Name and Units are mandatory string parameters
235                 auto nameFound = baseConfigMap.find("Name");
236                 if (nameFound == baseConfigMap.end())
237                 {
238                     lg2::error("Name parameter not found for '{PATH}'", "PATH",
239                                interfacePath);
240                     continue;
241                 }
242                 sensorName =
243                     std::visit(VariantToStringVisitor(), nameFound->second);
244                 if (sensorName.empty())
245                 {
246                     lg2::error("Name parameter not parsed for '{PATH}'", "PATH",
247                                interfacePath);
248                     continue;
249                 }
250 
251                 auto unitsFound = baseConfigMap.find("Units");
252                 if (unitsFound == baseConfigMap.end())
253                 {
254                     lg2::error("Units parameter not found for '{PATH}'", "PATH",
255                                interfacePath);
256                     continue;
257                 }
258                 sensorUnits =
259                     std::visit(VariantToStringVisitor(), unitsFound->second);
260                 if (sensorUnits.empty())
261                 {
262                     lg2::error("Units parameter not parsed for '{PATH}'",
263                                "PATH", interfacePath);
264                     continue;
265                 }
266 
267                 // on rescans, only update sensors we were signaled by
268                 auto findSensor = sensors.find(sensorName);
269                 if (!firstScan && (findSensor != sensors.end()))
270                 {
271                     std::string suffixName = "/";
272                     suffixName += findSensor->second->name;
273                     bool found = false;
274                     for (auto it = sensorsChanged->begin();
275                          it != sensorsChanged->end(); it++)
276                     {
277                         std::string suffixIt = "/";
278                         suffixIt += *it;
279                         if (suffixIt.ends_with(suffixName))
280                         {
281                             sensorsChanged->erase(it);
282                             findSensor->second = nullptr;
283                             found = true;
284                             lg2::debug("ExternalSensor '{NAME}' change found",
285                                        "NAME", sensorName);
286                             break;
287                         }
288                     }
289                     if (!found)
290                     {
291                         continue;
292                     }
293                 }
294 
295                 std::vector<thresholds::Threshold> sensorThresholds;
296                 if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
297                 {
298                     lg2::error("error populating thresholds for '{NAME}'",
299                                "NAME", sensorName);
300                 }
301 
302                 PowerState readState = getPowerState(baseConfigMap);
303 
304                 auto& sensorEntry = sensors[sensorName];
305                 sensorEntry = nullptr;
306 
307                 sensorEntry = std::make_shared<ExternalSensor>(
308                     sensorType, objectServer, dbusConnection, sensorName,
309                     sensorUnits, std::move(sensorThresholds), interfacePath,
310                     maxValue, minValue, timeoutSecs, readState);
311                 sensorEntry->initWriteHook(
312                     [&sensors, &reaperTimer](
313                         const std::chrono::steady_clock::time_point& now) {
314                         updateReaper(sensors, reaperTimer, now);
315                     });
316 
317                 lg2::debug("ExternalSensor '{NAME}' created", "NAME",
318                            sensorName);
319             }
320         });
321 
322     getter->getConfiguration(std::vector<std::string>{sensorType});
323 }
324 
main()325 int main()
326 {
327     lg2::debug("ExternalSensor service starting up");
328 
329     boost::asio::io_context io;
330     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
331     sdbusplus::asio::object_server objectServer(systemBus, true);
332 
333     objectServer.add_manager("/xyz/openbmc_project/sensors");
334     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
335 
336     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
337         sensors;
338     auto sensorsChanged =
339         std::make_shared<boost::container::flat_set<std::string>>();
340     boost::asio::steady_timer reaperTimer(io);
341 
342     boost::asio::post(io, [&objectServer, &sensors, &systemBus,
343                            &reaperTimer]() {
344         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
345     });
346 
347     boost::asio::steady_timer filterTimer(io);
348     std::function<void(sdbusplus::message_t&)> eventHandler =
349         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
350          &reaperTimer](sdbusplus::message_t& message) mutable {
351             if (message.is_method_error())
352             {
353                 lg2::error("callback method error");
354                 return;
355             }
356 
357             const auto* messagePath = message.get_path();
358             sensorsChanged->insert(messagePath);
359             lg2::debug("ExternalSensor change event received: '{PATH}'", "PATH",
360                        messagePath);
361 
362             // this implicitly cancels the timer
363             filterTimer.expires_after(std::chrono::seconds(1));
364 
365             filterTimer.async_wait(
366                 [&objectServer, &sensors, &systemBus, &sensorsChanged,
367                  &reaperTimer](const boost::system::error_code& ec) mutable {
368                     if (ec != boost::system::errc::success)
369                     {
370                         if (ec != boost::asio::error::operation_aborted)
371                         {
372                             lg2::error("callback error: '{ERROR_MESSAGE}'",
373                                        "ERROR_MESSAGE", ec.message());
374                         }
375                         return;
376                     }
377 
378                     createSensors(objectServer, sensors, systemBus,
379                                   sensorsChanged, reaperTimer);
380                 });
381         };
382 
383     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
384         setupPropertiesChangedMatches(
385             *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
386 
387     lg2::debug("ExternalSensor service entering main loop");
388 
389     io.run();
390 }
391