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