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