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