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 <sdbusplus/asio/connection.hpp>
13 #include <sdbusplus/asio/object_server.hpp>
14 #include <sdbusplus/bus/match.hpp>
15 #include <sdbusplus/message.hpp>
16 #include <sdbusplus/message/native_types.hpp>
17 
18 #include <algorithm>
19 #include <array>
20 #include <chrono>
21 #include <cmath>
22 #include <functional>
23 #include <iostream>
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 constexpr bool debug = false;
59 
60 static const char* sensorType = "ExternalSensor";
61 
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)62 void updateReaper(boost::container::flat_map<
63                       std::string, std::shared_ptr<ExternalSensor>>& 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         if constexpr (debug)
120         {
121             std::cerr << "Next ExternalSensor timer idle\n";
122         }
123 
124         return;
125     }
126 
127     timer.expires_at(now + nextCheck);
128 
129     timer.async_wait([&sensors, &timer](const boost::system::error_code& err) {
130         if (err != boost::system::errc::success)
131         {
132             // Cancellation is normal, as timer is dynamically rescheduled
133             if (err != boost::asio::error::operation_aborted)
134             {
135                 std::cerr << "ExternalSensor timer scheduling problem: "
136                           << err.message() << "\n";
137             }
138             return;
139         }
140 
141         updateReaper(sensors, timer, std::chrono::steady_clock::now());
142     });
143 
144     if constexpr (debug)
145     {
146         std::cerr << "Next ExternalSensor timer "
147                   << std::chrono::duration_cast<std::chrono::microseconds>(
148                          nextCheck)
149                          .count()
150                   << " us\n";
151     }
152 }
153 
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)154 void createSensors(
155     sdbusplus::asio::object_server& objectServer,
156     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
157         sensors,
158     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
159     const std::shared_ptr<boost::container::flat_set<std::string>>&
160         sensorsChanged,
161     boost::asio::steady_timer& reaperTimer)
162 {
163     if constexpr (debug)
164     {
165         std::cerr << "ExternalSensor considering creating sensors\n";
166     }
167 
168     auto getter = std::make_shared<GetSensorConfiguration>(
169         dbusConnection,
170         [&objectServer, &sensors, &dbusConnection, sensorsChanged,
171          &reaperTimer](const ManagedObjectType& sensorConfigurations) {
172         bool firstScan = (sensorsChanged == nullptr);
173 
174         for (const std::pair<sdbusplus::message::object_path, SensorData>&
175                  sensor : sensorConfigurations)
176         {
177             const std::string& interfacePath = sensor.first.str;
178             const SensorData& sensorData = sensor.second;
179 
180             auto sensorBase = sensorData.find(configInterfaceName(sensorType));
181             if (sensorBase == sensorData.end())
182             {
183                 std::cerr << "Base configuration not found for "
184                           << interfacePath << "\n";
185                 continue;
186             }
187 
188             const SensorBaseConfiguration& baseConfiguration = *sensorBase;
189             const SensorBaseConfigMap& baseConfigMap = baseConfiguration.second;
190 
191             // MinValue and MinValue are mandatory numeric parameters
192             auto minFound = baseConfigMap.find("MinValue");
193             if (minFound == baseConfigMap.end())
194             {
195                 std::cerr << "MinValue parameter not found for "
196                           << interfacePath << "\n";
197                 continue;
198             }
199             double minValue = std::visit(VariantToDoubleVisitor(),
200                                          minFound->second);
201             if (!std::isfinite(minValue))
202             {
203                 std::cerr << "MinValue parameter not parsed for "
204                           << interfacePath << "\n";
205                 continue;
206             }
207 
208             auto maxFound = baseConfigMap.find("MaxValue");
209             if (maxFound == baseConfigMap.end())
210             {
211                 std::cerr << "MaxValue parameter not found for "
212                           << interfacePath << "\n";
213                 continue;
214             }
215             double maxValue = std::visit(VariantToDoubleVisitor(),
216                                          maxFound->second);
217             if (!std::isfinite(maxValue))
218             {
219                 std::cerr << "MaxValue parameter not parsed for "
220                           << interfacePath << "\n";
221                 continue;
222             }
223 
224             double timeoutSecs = 0.0;
225 
226             // Timeout is an optional numeric parameter
227             auto timeoutFound = baseConfigMap.find("Timeout");
228             if (timeoutFound != baseConfigMap.end())
229             {
230                 timeoutSecs = std::visit(VariantToDoubleVisitor(),
231                                          timeoutFound->second);
232             }
233             if (!std::isfinite(timeoutSecs) || (timeoutSecs < 0.0))
234             {
235                 std::cerr << "Timeout parameter not parsed for "
236                           << interfacePath << "\n";
237                 continue;
238             }
239 
240             std::string sensorName;
241             std::string sensorUnits;
242 
243             // Name and Units are mandatory string parameters
244             auto nameFound = baseConfigMap.find("Name");
245             if (nameFound == baseConfigMap.end())
246             {
247                 std::cerr << "Name parameter not found for " << interfacePath
248                           << "\n";
249                 continue;
250             }
251             sensorName = std::visit(VariantToStringVisitor(),
252                                     nameFound->second);
253             if (sensorName.empty())
254             {
255                 std::cerr << "Name parameter not parsed for " << interfacePath
256                           << "\n";
257                 continue;
258             }
259 
260             auto unitsFound = baseConfigMap.find("Units");
261             if (unitsFound == baseConfigMap.end())
262             {
263                 std::cerr << "Units parameter not found for " << interfacePath
264                           << "\n";
265                 continue;
266             }
267             sensorUnits = std::visit(VariantToStringVisitor(),
268                                      unitsFound->second);
269             if (sensorUnits.empty())
270             {
271                 std::cerr << "Units parameter not parsed for " << interfacePath
272                           << "\n";
273                 continue;
274             }
275 
276             // on rescans, only update sensors we were signaled by
277             auto findSensor = sensors.find(sensorName);
278             if (!firstScan && (findSensor != sensors.end()))
279             {
280                 std::string suffixName = "/";
281                 suffixName += findSensor->second->name;
282                 bool found = false;
283                 for (auto it = sensorsChanged->begin();
284                      it != sensorsChanged->end(); it++)
285                 {
286                     std::string suffixIt = "/";
287                     suffixIt += *it;
288                     if (suffixIt.ends_with(suffixName))
289                     {
290                         sensorsChanged->erase(it);
291                         findSensor->second = nullptr;
292                         found = true;
293                         if constexpr (debug)
294                         {
295                             std::cerr << "ExternalSensor " << sensorName
296                                       << " change found\n";
297                         }
298                         break;
299                     }
300                 }
301                 if (!found)
302                 {
303                     continue;
304                 }
305             }
306 
307             std::vector<thresholds::Threshold> sensorThresholds;
308             if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
309             {
310                 std::cerr << "error populating thresholds for " << sensorName
311                           << "\n";
312             }
313 
314             PowerState readState = getPowerState(baseConfigMap);
315 
316             auto& sensorEntry = sensors[sensorName];
317             sensorEntry = nullptr;
318 
319             sensorEntry = std::make_shared<ExternalSensor>(
320                 sensorType, objectServer, dbusConnection, sensorName,
321                 sensorUnits, std::move(sensorThresholds), interfacePath,
322                 maxValue, minValue, timeoutSecs, readState);
323             sensorEntry->initWriteHook(
324                 [&sensors, &reaperTimer](
325                     const std::chrono::steady_clock::time_point& now) {
326                 updateReaper(sensors, reaperTimer, now);
327             });
328 
329             if constexpr (debug)
330             {
331                 std::cerr << "ExternalSensor " << sensorName << " created\n";
332             }
333         }
334     });
335 
336     getter->getConfiguration(std::vector<std::string>{sensorType});
337 }
338 
main()339 int main()
340 {
341     if constexpr (debug)
342     {
343         std::cerr << "ExternalSensor service starting up\n";
344     }
345 
346     boost::asio::io_context io;
347     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
348     sdbusplus::asio::object_server objectServer(systemBus, true);
349 
350     objectServer.add_manager("/xyz/openbmc_project/sensors");
351     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
352 
353     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
354         sensors;
355     auto sensorsChanged =
356         std::make_shared<boost::container::flat_set<std::string>>();
357     boost::asio::steady_timer reaperTimer(io);
358 
359     boost::asio::post(io,
360                       [&objectServer, &sensors, &systemBus, &reaperTimer]() {
361         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
362     });
363 
364     boost::asio::steady_timer filterTimer(io);
365     std::function<void(sdbusplus::message_t&)> eventHandler =
366         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
367          &reaperTimer](sdbusplus::message_t& message) mutable {
368         if (message.is_method_error())
369         {
370             std::cerr << "callback method error\n";
371             return;
372         }
373 
374         const auto* messagePath = message.get_path();
375         sensorsChanged->insert(messagePath);
376         if constexpr (debug)
377         {
378             std::cerr << "ExternalSensor change event received: " << messagePath
379                       << "\n";
380         }
381 
382         // this implicitly cancels the timer
383         filterTimer.expires_after(std::chrono::seconds(1));
384 
385         filterTimer.async_wait(
386             [&objectServer, &sensors, &systemBus, &sensorsChanged,
387              &reaperTimer](const boost::system::error_code& ec) mutable {
388             if (ec != boost::system::errc::success)
389             {
390                 if (ec != boost::asio::error::operation_aborted)
391                 {
392                     std::cerr << "callback error: " << ec.message() << "\n";
393                 }
394                 return;
395             }
396 
397             createSensors(objectServer, sensors, systemBus, sensorsChanged,
398                           reaperTimer);
399         });
400     };
401 
402     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
403         setupPropertiesChangedMatches(
404             *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
405 
406     if constexpr (debug)
407     {
408         std::cerr << "ExternalSensor service entering main loop\n";
409     }
410 
411     io.run();
412 }
413