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 = baseConfiguration.second;
186 
187             // MinValue and MinValue are mandatory numeric parameters
188             auto minFound = baseConfigMap.find("MinValue");
189             if (minFound == baseConfigMap.end())
190             {
191                 std::cerr << "MinValue parameter not found for "
192                           << interfacePath << "\n";
193                 continue;
194             }
195             double minValue =
196                 std::visit(VariantToDoubleVisitor(), minFound->second);
197             if (!std::isfinite(minValue))
198             {
199                 std::cerr << "MinValue parameter not parsed for "
200                           << interfacePath << "\n";
201                 continue;
202             }
203 
204             auto maxFound = baseConfigMap.find("MaxValue");
205             if (maxFound == baseConfigMap.end())
206             {
207                 std::cerr << "MaxValue parameter not found for "
208                           << interfacePath << "\n";
209                 continue;
210             }
211             double maxValue =
212                 std::visit(VariantToDoubleVisitor(), maxFound->second);
213             if (!std::isfinite(maxValue))
214             {
215                 std::cerr << "MaxValue parameter not parsed for "
216                           << interfacePath << "\n";
217                 continue;
218             }
219 
220             double timeoutSecs = 0.0;
221 
222             // Timeout is an optional numeric parameter
223             auto timeoutFound = baseConfigMap.find("Timeout");
224             if (timeoutFound != baseConfigMap.end())
225             {
226                 timeoutSecs =
227                     std::visit(VariantToDoubleVisitor(), timeoutFound->second);
228             }
229             if (!(std::isfinite(timeoutSecs) && (timeoutSecs >= 0.0)))
230             {
231                 std::cerr << "Timeout parameter not parsed for "
232                           << interfacePath << "\n";
233                 continue;
234             }
235 
236             std::string sensorName;
237             std::string sensorUnits;
238 
239             // Name and Units are mandatory string parameters
240             auto nameFound = baseConfigMap.find("Name");
241             if (nameFound == baseConfigMap.end())
242             {
243                 std::cerr << "Name parameter not found for " << interfacePath
244                           << "\n";
245                 continue;
246             }
247             sensorName =
248                 std::visit(VariantToStringVisitor(), nameFound->second);
249             if (sensorName.empty())
250             {
251                 std::cerr << "Name parameter not parsed for " << interfacePath
252                           << "\n";
253                 continue;
254             }
255 
256             auto unitsFound = baseConfigMap.find("Units");
257             if (unitsFound == baseConfigMap.end())
258             {
259                 std::cerr << "Units parameter not found for " << interfacePath
260                           << "\n";
261                 continue;
262             }
263             sensorUnits =
264                 std::visit(VariantToStringVisitor(), unitsFound->second);
265             if (sensorUnits.empty())
266             {
267                 std::cerr << "Units parameter not parsed for " << interfacePath
268                           << "\n";
269                 continue;
270             }
271 
272             // on rescans, only update sensors we were signaled by
273             auto findSensor = sensors.find(sensorName);
274             if (!firstScan && (findSensor != sensors.end()))
275             {
276                 std::string suffixName = "/";
277                 suffixName += findSensor->second->name;
278                 bool found = false;
279                 for (auto it = sensorsChanged->begin();
280                      it != sensorsChanged->end(); it++)
281                 {
282                     std::string suffixIt = "/";
283                     suffixIt += *it;
284                     if (boost::ends_with(suffixIt, suffixName))
285                     {
286                         sensorsChanged->erase(it);
287                         findSensor->second = nullptr;
288                         found = true;
289                         if constexpr (debug)
290                         {
291                             std::cerr << "ExternalSensor " << sensorName
292                                       << " change found\n";
293                         }
294                         break;
295                     }
296                 }
297                 if (!found)
298                 {
299                     continue;
300                 }
301             }
302 
303             std::vector<thresholds::Threshold> sensorThresholds;
304             if (!parseThresholdsFromConfig(sensorData, sensorThresholds))
305             {
306                 std::cerr << "error populating thresholds for " << sensorName
307                           << "\n";
308             }
309 
310             PowerState readState = getPowerState(baseConfigMap);
311 
312             auto& sensorEntry = sensors[sensorName];
313             sensorEntry = nullptr;
314 
315             sensorEntry = std::make_shared<ExternalSensor>(
316                 sensorType, objectServer, dbusConnection, sensorName,
317                 sensorUnits, std::move(sensorThresholds), interfacePath,
318                 maxValue, minValue, timeoutSecs, readState);
319             sensorEntry->initWriteHook(
320                 [&sensors, &reaperTimer](
321                     const std::chrono::steady_clock::time_point& now) {
322                 updateReaper(sensors, reaperTimer, now);
323             });
324 
325             if constexpr (debug)
326             {
327                 std::cerr << "ExternalSensor " << sensorName << " created\n";
328             }
329         }
330         });
331 
332     getter->getConfiguration(std::vector<std::string>{sensorType});
333 }
334 
335 int main()
336 {
337     if constexpr (debug)
338     {
339         std::cerr << "ExternalSensor service starting up\n";
340     }
341 
342     boost::asio::io_service io;
343     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
344     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
345     sdbusplus::asio::object_server objectServer(systemBus);
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     io.post([&objectServer, &sensors, &systemBus, &reaperTimer]() {
353         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
354     });
355 
356     boost::asio::deadline_timer filterTimer(io);
357     std::function<void(sdbusplus::message_t&)> eventHandler =
358         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
359          &reaperTimer](sdbusplus::message_t& message) mutable {
360         if (message.is_method_error())
361         {
362             std::cerr << "callback method error\n";
363             return;
364         }
365 
366         const auto* messagePath = message.get_path();
367         sensorsChanged->insert(messagePath);
368         if constexpr (debug)
369         {
370             std::cerr << "ExternalSensor change event received: " << messagePath
371                       << "\n";
372         }
373 
374         // this implicitly cancels the timer
375         filterTimer.expires_from_now(boost::posix_time::seconds(1));
376 
377         filterTimer.async_wait(
378             [&objectServer, &sensors, &systemBus, &sensorsChanged,
379              &reaperTimer](const boost::system::error_code& ec) mutable {
380             if (ec != boost::system::errc::success)
381             {
382                 if (ec != boost::asio::error::operation_aborted)
383                 {
384                     std::cerr << "callback error: " << ec.message() << "\n";
385                 }
386                 return;
387             }
388 
389             createSensors(objectServer, sensors, systemBus, sensorsChanged,
390                           reaperTimer);
391         });
392     };
393 
394     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches =
395         setupPropertiesChangedMatches(
396             *systemBus, std::to_array<const char*>({sensorType}), eventHandler);
397 
398     if constexpr (debug)
399     {
400         std::cerr << "ExternalSensor service entering main loop\n";
401     }
402 
403     io.run();
404 }
405