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             auto findPowerOn = baseConfiguration.second.find("PowerState");
311             PowerState readState = PowerState::always;
312             if (findPowerOn != baseConfiguration.second.end())
313             {
314                 std::string powerState =
315                     std::visit(VariantToStringVisitor(), findPowerOn->second);
316                 setReadState(powerState, readState);
317             }
318 
319             auto& sensorEntry = sensors[sensorName];
320             sensorEntry = nullptr;
321 
322             sensorEntry = std::make_shared<ExternalSensor>(
323                 sensorType, objectServer, dbusConnection, sensorName,
324                 sensorUnits, std::move(sensorThresholds), interfacePath,
325                 maxValue, minValue, timeoutSecs, readState);
326             sensorEntry->initWriteHook(
327                 [&sensors, &reaperTimer](
328                     const std::chrono::steady_clock::time_point& now) {
329                 updateReaper(sensors, reaperTimer, now);
330             });
331 
332             if constexpr (debug)
333             {
334                 std::cerr << "ExternalSensor " << sensorName << " created\n";
335             }
336         }
337         });
338 
339     getter->getConfiguration(std::vector<std::string>{sensorType});
340 }
341 
342 int main()
343 {
344     if constexpr (debug)
345     {
346         std::cerr << "ExternalSensor service starting up\n";
347     }
348 
349     boost::asio::io_service io;
350     auto systemBus = std::make_shared<sdbusplus::asio::connection>(io);
351     systemBus->request_name("xyz.openbmc_project.ExternalSensor");
352     sdbusplus::asio::object_server objectServer(systemBus);
353     boost::container::flat_map<std::string, std::shared_ptr<ExternalSensor>>
354         sensors;
355     std::vector<std::unique_ptr<sdbusplus::bus::match::match>> matches;
356     auto sensorsChanged =
357         std::make_shared<boost::container::flat_set<std::string>>();
358     boost::asio::steady_timer reaperTimer(io);
359 
360     io.post([&objectServer, &sensors, &systemBus, &reaperTimer]() {
361         createSensors(objectServer, sensors, systemBus, nullptr, reaperTimer);
362     });
363 
364     boost::asio::deadline_timer filterTimer(io);
365     std::function<void(sdbusplus::message::message&)> eventHandler =
366         [&objectServer, &sensors, &systemBus, &sensorsChanged, &filterTimer,
367          &reaperTimer](sdbusplus::message::message& message) mutable {
368         if (message.is_method_error())
369         {
370             std::cerr << "callback method error\n";
371             return;
372         }
373 
374         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_from_now(boost::posix_time::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     auto match = std::make_unique<sdbusplus::bus::match::match>(
403         static_cast<sdbusplus::bus::bus&>(*systemBus),
404         "type='signal',member='PropertiesChanged',path_namespace='" +
405             std::string(inventoryPath) + "',arg0namespace='" + sensorType + "'",
406         eventHandler);
407     matches.emplace_back(std::move(match));
408 
409     if constexpr (debug)
410     {
411         std::cerr << "ExternalSensor service entering main loop\n";
412     }
413 
414     io.run();
415 }
416