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