1 #include "ExternalSensor.hpp"
2 #include "Utils.hpp"
3 #include "VariantVisitors.hpp"
4 
5 #include <boost/algorithm/string/predicate.hpp>
6 #include <boost/algorithm/string/replace.hpp>
7 #include <boost/container/flat_map.hpp>
8 #include <boost/container/flat_set.hpp>
9 #include <sdbusplus/asio/connection.hpp>
10 #include <sdbusplus/asio/object_server.hpp>
11 #include <sdbusplus/bus/match.hpp>
12 
13 #include <array>
14 #include <filesystem>
15 #include <fstream>
16 #include <functional>
17 #include <memory>
18 #include <regex>
19 #include <stdexcept>
20 #include <string>
21 #include <utility>
22 #include <variant>
23 #include <vector>
24 
25 // Copied from HwmonTempSensor and inspired by
26 // https://gerrit.openbmc-project.xyz/c/openbmc/dbus-sensors/+/35476
27 
28 // The ExternalSensor is a sensor whose value is intended to be writable
29 // by something external to the BMC, so that the host (or something else)
30 // can write to it, perhaps by using an IPMI or Redfish connection.
31 
32 // Unlike most other sensors, an external sensor does not correspond
33 // to a hwmon file or any other kernel/hardware interface,
34 // so, after initialization, this module does not have much to do,
35 // but it handles reinitialization and thresholds, similar to the others.
36 // The main work of this module is to provide backing storage for a
37 // sensor that exists only virtually, and to provide an optional
38 // timeout service for detecting loss of timely updates.
39 
40 // As there is no corresponding driver or hardware to support,
41 // all configuration of this sensor comes from the JSON parameters:
42 // MinValue, MaxValue, Timeout, PowerState, Units, Name
43 
44 // The purpose of "Units" is to specify the physical characteristic
45 // the external sensor is measuring, because with an external sensor
46 // there is no other way to tell, and it will be used for the object path
47 // here: /xyz/openbmc_project/sensors/<Units>/<Name>
48 
49 // For more information, see external-sensor.md design document:
50 // https://gerrit.openbmc-project.xyz/c/openbmc/docs/+/41452
51 // https://github.com/openbmc/docs/tree/master/designs/
52 
53 static constexpr bool debug = false;
54 
55 static const char* sensorType =
56     "xyz.openbmc_project.Configuration.ExternalSensor";
57 
58 void updateReaper(boost::container::flat_map<
59                       std::string, std::shared_ptr<ExternalSensor>>& sensors,
60                   boost::asio::steady_timer& timer,
61                   const std::chrono::steady_clock::time_point& now)
62 {
63     // First pass, reap all stale sensors
64     for (auto& sensor : sensors)
65     {
66         if (!sensor.second)
67         {
68             continue;
69         }
70 
71         if (!sensor.second->isAliveAndPerishable())
72         {
73             continue;
74         }
75 
76         if (!sensor.second->isAliveAndFresh(now))
77         {
78             // Mark sensor as dead, no longer alive
79             sensor.second->writeInvalidate();
80         }
81     }
82 
83     std::chrono::steady_clock::duration nextCheck;
84     bool needCheck = false;
85 
86     // Second pass, determine timer interval to next check
87     for (auto& sensor : sensors)
88     {
89         if (!sensor.second)
90         {
91             continue;
92         }
93 
94         if (!sensor.second->isAliveAndPerishable())
95         {
96             continue;
97         }
98 
99         auto expiration = sensor.second->ageRemaining(now);
100 
101         if (needCheck)
102         {
103             nextCheck = std::min(nextCheck, expiration);
104         }
105         else
106         {
107             // Initialization
108             nextCheck = expiration;
109             needCheck = true;
110         }
111     }
112 
113     if (!needCheck)
114     {
115         if constexpr (debug)
116         {
117             std::cerr << "Next ExternalSensor timer idle\n";
118         }
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                 std::cerr << "ExternalSensor timer scheduling problem: "
132                           << err.message() << "\n";
133             }
134             return;
135         }
136 
137         updateReaper(sensors, timer, std::chrono::steady_clock::now());
138     });
139 
140     if constexpr (debug)
141     {
142         std::cerr << "Next ExternalSensor timer "
143                   << std::chrono::duration_cast<std::chrono::microseconds>(
144                          nextCheck)
145                          .count()
146                   << " us\n";
147     }
148 }
149 
150 void createSensors(
151     sdbusplus::asio::object_server& objectServer,
152     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>&
153         sensors,
154     std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
155     const std::shared_ptr<boost::container::flat_set<std::string>>&
156         sensorsChanged,
157     boost::asio::steady_timer& reaperTimer)
158 {
159     if constexpr (debug)
160     {
161         std::cerr << "ExternalSensor considering creating sensors\n";
162     }
163 
164     auto getter = std::make_shared<GetSensorConfiguration>(
165         dbusConnection,
166         [&objectServer, &sensors, &dbusConnection, sensorsChanged,
167          &reaperTimer](const ManagedObjectType& sensorConfigurations) {
168             bool firstScan = (sensorsChanged == nullptr);
169 
170             for (const std::pair<sdbusplus::message::object_path, SensorData>&
171                      sensor : sensorConfigurations)
172             {
173                 const std::string& interfacePath = sensor.first.str;
174                 const SensorData& sensorData = sensor.second;
175 
176                 auto sensorBase = sensorData.find(sensorType);
177                 if (sensorBase == sensorData.end())
178                 {
179                     std::cerr << "Base configuration not found for "
180                               << interfacePath << "\n";
181                     continue;
182                 }
183 
184                 const SensorBaseConfiguration& baseConfiguration = *sensorBase;
185                 const SensorBaseConfigMap& baseConfigMap =
186                     baseConfiguration.second;
187 
188                 double minValue;
189                 double maxValue;
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                 minValue =
200                     std::visit(VariantToDoubleVisitor(), 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                 maxValue =
216                     std::visit(VariantToDoubleVisitor(), 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 "
248                               << interfacePath << "\n";
249                     continue;
250                 }
251                 sensorName =
252                     std::visit(VariantToStringVisitor(), nameFound->second);
253                 if (sensorName.empty())
254                 {
255                     std::cerr << "Name parameter not parsed for "
256                               << interfacePath << "\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 "
264                               << interfacePath << "\n";
265                     continue;
266                 }
267                 sensorUnits =
268                     std::visit(VariantToStringVisitor(), unitsFound->second);
269                 if (sensorUnits.empty())
270                 {
271                     std::cerr << "Units parameter not parsed for "
272                               << interfacePath << "\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 (boost::ends_with(suffixIt, 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 "
311                               << sensorName << "\n";
312                 }
313 
314                 auto findPowerOn = baseConfiguration.second.find("PowerState");
315                 PowerState readState = PowerState::always;
316                 if (findPowerOn != baseConfiguration.second.end())
317                 {
318                     std::string powerState = std::visit(
319                         VariantToStringVisitor(), findPowerOn->second);
320                     setReadState(powerState, readState);
321                 }
322 
323                 auto& sensorEntry = sensors[sensorName];
324                 sensorEntry = nullptr;
325 
326                 sensorEntry = std::make_shared<ExternalSensor>(
327                     sensorType, objectServer, dbusConnection, sensorName,
328                     sensorUnits, std::move(sensorThresholds), interfacePath,
329                     maxValue, minValue, timeoutSecs, readState);
330                 sensorEntry->initWriteHook(
331                     [&sensors, &reaperTimer](
332                         const std::chrono::steady_clock::time_point& now) {
333                         updateReaper(sensors, reaperTimer, now);
334                     });
335 
336                 if constexpr (debug)
337                 {
338                     std::cerr << "ExternalSensor " << sensorName
339                               << " created\n";
340                 }
341             }
342         });
343 
344     getter->getConfiguration(std::vector<std::string>{sensorType});
345 }
346 
347 int main()
348 {
349     if constexpr (debug)
350     {
351         std::cerr << "ExternalSensor service starting up\n";
352     }
353 
354     boost::asio::io_service io;
355     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
356     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
357     sdbusplus::asio::object_server objectServer(systemBus);
358     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
359         sensors;
360     std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
361     auto sensorsChanged =
362         std::make_shared<boost::container::flat_set<std::string>>();
363     boost::asio::steady_timer reaperTimer(io);
364 
365     io.post([&objectServer, &sensors, &systemBus, &reaperTimer]() {
366         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
367     });
368 
369     boost::asio::deadline_timer filterTimer(io);
370     std::function<void(sdbusplus::message::message&)> eventHandler =
371         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
372          &reaperTimer](sdbusplus::message::message& message) mutable {
373             if (message.is_method_error())
374             {
375                 std::cerr << "callback method error\n";
376                 return;
377             }
378 
379             auto messagePath = message.get_path();
380             sensorsChanged->insert(messagePath);
381             if constexpr (debug)
382             {
383                 std::cerr << "ExternalSensor change event received: "
384                           << messagePath << "\n";
385             }
386 
387             // this implicitly cancels the timer
388             filterTimer.expires_from_now(boost::posix_time::seconds(1));
389 
390             filterTimer.async_wait(
391                 [&objectServer, &sensors, &systemBus, &sensorsChanged,
392                  &reaperTimer](const boost::system::error_code& ec) mutable {
393                     if (ec != boost::system::errc::success)
394                     {
395                         if (ec != boost::asio::error::operation_aborted)
396                         {
397                             std::cerr << "callback error: " << ec.message()
398                                       << "\n";
399                         }
400                         return;
401                     }
402 
403                     createSensors(objectServer, sensors, systemBus,
404                                   sensorsChanged, reaperTimer);
405                 });
406         };
407 
408     auto match = std::make_unique<sdbusplus::bus::match::match>(
409         static_cast<sdbusplus::bus::bus&>(*systemBus),
410         "type='signal',member='PropertiesChanged',path_namespace='" +
411             std::string(inventoryPath) + "',arg0namespace='" + sensorType + "'",
412         eventHandler);
413     matches.emplace_back(std::move(match));
414 
415     if constexpr (debug)
416     {
417         std::cerr << "ExternalSensor service entering main loop\n";
418     }
419 
420     io.run();
421 }
422