1 /*
2 // Copyright (c) 2018 Intel Corporation
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 //      http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 */
16 
17 #include "conf.hpp"
18 #include "dbus/util.hpp"
19 
20 #include <algorithm>
21 #include <chrono>
22 #include <functional>
23 #include <iostream>
24 #include <regex>
25 #include <sdbusplus/bus.hpp>
26 #include <sdbusplus/bus/match.hpp>
27 #include <sdbusplus/exception.hpp>
28 #include <set>
29 #include <thread>
30 #include <unordered_map>
31 #include <variant>
32 
33 static constexpr bool DEBUG = false; // enable to print found configuration
34 
35 extern std::map<std::string, struct conf::SensorConfig> sensorConfig;
36 extern std::map<int64_t, conf::PIDConf> zoneConfig;
37 extern std::map<int64_t, struct conf::ZoneConfig> zoneDetailsConfig;
38 
39 constexpr const char* pidConfigurationInterface =
40     "xyz.openbmc_project.Configuration.Pid";
41 constexpr const char* objectManagerInterface =
42     "org.freedesktop.DBus.ObjectManager";
43 constexpr const char* pidZoneConfigurationInterface =
44     "xyz.openbmc_project.Configuration.Pid.Zone";
45 constexpr const char* stepwiseConfigurationInterface =
46     "xyz.openbmc_project.Configuration.Stepwise";
47 constexpr const char* sensorInterface = "xyz.openbmc_project.Sensor.Value";
48 constexpr const char* pwmInterface = "xyz.openbmc_project.Control.FanPwm";
49 
50 namespace dbus_configuration
51 {
52 
53 bool findSensors(const std::unordered_map<std::string, std::string>& sensors,
54                  const std::string& search,
55                  std::vector<std::pair<std::string, std::string>>& matches)
56 {
57     std::smatch match;
58     std::regex reg(search);
59     for (const auto& sensor : sensors)
60     {
61         if (std::regex_search(sensor.first, match, reg))
62         {
63             matches.push_back(sensor);
64         }
65     }
66 
67     return matches.size() > 0;
68 }
69 
70 // this function prints the configuration into a form similar to the cpp
71 // generated code to help in verification, should be turned off during normal
72 // use
73 void debugPrint(void)
74 {
75     // print sensor config
76     std::cout << "sensor config:\n";
77     std::cout << "{\n";
78     for (const auto& pair : sensorConfig)
79     {
80 
81         std::cout << "\t{" << pair.first << ",\n\t\t{";
82         std::cout << pair.second.type << ", ";
83         std::cout << pair.second.readPath << ", ";
84         std::cout << pair.second.writePath << ", ";
85         std::cout << pair.second.min << ", ";
86         std::cout << pair.second.max << ", ";
87         std::cout << pair.second.timeout << "},\n\t},\n";
88     }
89     std::cout << "}\n\n";
90     std::cout << "ZoneDetailsConfig\n";
91     std::cout << "{\n";
92     for (const auto& zone : zoneDetailsConfig)
93     {
94         std::cout << "\t{" << zone.first << ",\n";
95         std::cout << "\t\t{" << zone.second.minThermalOutput << ", ";
96         std::cout << zone.second.failsafePercent << "}\n\t},\n";
97     }
98     std::cout << "}\n\n";
99     std::cout << "ZoneConfig\n";
100     std::cout << "{\n";
101     for (const auto& zone : zoneConfig)
102     {
103         std::cout << "\t{" << zone.first << "\n";
104         for (const auto& pidconf : zone.second)
105         {
106             std::cout << "\t\t{" << pidconf.first << ",\n";
107             std::cout << "\t\t\t{" << pidconf.second.type << ",\n";
108             std::cout << "\t\t\t{";
109             for (const auto& input : pidconf.second.inputs)
110             {
111                 std::cout << "\n\t\t\t" << input << ",\n";
112             }
113             std::cout << "\t\t\t}\n";
114             std::cout << "\t\t\t" << pidconf.second.setpoint << ",\n";
115             std::cout << "\t\t\t{" << pidconf.second.pidInfo.ts << ",\n";
116             std::cout << "\t\t\t" << pidconf.second.pidInfo.proportionalCoeff
117                       << ",\n";
118             std::cout << "\t\t\t" << pidconf.second.pidInfo.integralCoeff
119                       << ",\n";
120             std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdOffset
121                       << ",\n";
122             std::cout << "\t\t\t" << pidconf.second.pidInfo.feedFwdGain
123                       << ",\n";
124             std::cout << "\t\t\t{" << pidconf.second.pidInfo.integralLimit.min
125                       << "," << pidconf.second.pidInfo.integralLimit.max
126                       << "},\n";
127             std::cout << "\t\t\t{" << pidconf.second.pidInfo.outLim.min << ","
128                       << pidconf.second.pidInfo.outLim.max << "},\n";
129             std::cout << "\t\t\t" << pidconf.second.pidInfo.slewNeg << ",\n";
130             std::cout << "\t\t\t" << pidconf.second.pidInfo.slewPos << ",\n";
131             std::cout << "\t\t\t}\n\t\t}\n";
132         }
133         std::cout << "\t},\n";
134     }
135     std::cout << "}\n\n";
136 }
137 
138 int eventHandler(sd_bus_message*, void*, sd_bus_error*)
139 {
140     // do a brief sleep as we tend to get a bunch of these events at
141     // once
142     std::this_thread::sleep_for(std::chrono::seconds(5));
143     std::cout << "New configuration detected, restarting\n.";
144     std::exit(EXIT_SUCCESS); // service file should make us restart
145     return 1;
146 }
147 
148 size_t getZoneIndex(const std::string& name, std::vector<std::string>& zones)
149 {
150     auto it = std::find(zones.begin(), zones.end(), name);
151     if (it == zones.end())
152     {
153         zones.emplace_back(name);
154         it = zones.end() - 1;
155     }
156 
157     return it - zones.begin();
158 }
159 
160 void init(sdbusplus::bus::bus& bus)
161 {
162     using DbusVariantType =
163         std::variant<uint64_t, int64_t, double, std::string,
164                      std::vector<std::string>, std::vector<double>>;
165 
166     using ManagedObjectType = std::unordered_map<
167         sdbusplus::message::object_path,
168         std::unordered_map<std::string,
169                            std::unordered_map<std::string, DbusVariantType>>>;
170 
171     // restart on configuration properties changed
172     static sdbusplus::bus::match::match configMatch(
173         bus,
174         "type='signal',member='PropertiesChanged',arg0namespace='" +
175             std::string(pidConfigurationInterface) + "'",
176         eventHandler);
177 
178     // restart on sensors changed
179     static sdbusplus::bus::match::match sensorAdded(
180         bus,
181         "type='signal',member='InterfacesAdded',arg0path='/xyz/openbmc_project/"
182         "sensors/'",
183         eventHandler);
184 
185     auto mapper =
186         bus.new_method_call("xyz.openbmc_project.ObjectMapper",
187                             "/xyz/openbmc_project/object_mapper",
188                             "xyz.openbmc_project.ObjectMapper", "GetSubTree");
189     mapper.append("/", 0,
190                   std::array<const char*, 6>{objectManagerInterface,
191                                              pidConfigurationInterface,
192                                              pidZoneConfigurationInterface,
193                                              stepwiseConfigurationInterface,
194                                              sensorInterface, pwmInterface});
195     std::unordered_map<
196         std::string, std::unordered_map<std::string, std::vector<std::string>>>
197         respData;
198     try
199     {
200         auto resp = bus.call(mapper);
201         resp.read(respData);
202     }
203     catch (sdbusplus::exception_t&)
204     {
205         // can't do anything without mapper call data
206         throw std::runtime_error("ObjectMapper Call Failure");
207     }
208 
209     if (respData.empty())
210     {
211         // can't do anything without mapper call data
212         throw std::runtime_error("No configuration data available from Mapper");
213     }
214     // create a map of pair of <has pid configuration, ObjectManager path>
215     std::unordered_map<std::string, std::pair<bool, std::string>> owners;
216     // and a map of <path, interface> for sensors
217     std::unordered_map<std::string, std::string> sensors;
218     for (const auto& objectPair : respData)
219     {
220         for (const auto& ownerPair : objectPair.second)
221         {
222             auto& owner = owners[ownerPair.first];
223             for (const std::string& interface : ownerPair.second)
224             {
225 
226                 if (interface == objectManagerInterface)
227                 {
228                     owner.second = objectPair.first;
229                 }
230                 if (interface == pidConfigurationInterface ||
231                     interface == pidZoneConfigurationInterface ||
232                     interface == stepwiseConfigurationInterface)
233                 {
234                     owner.first = true;
235                 }
236                 if (interface == sensorInterface || interface == pwmInterface)
237                 {
238                     // we're not interested in pwm sensors, just pwm control
239                     if (interface == sensorInterface &&
240                         objectPair.first.find("pwm") != std::string::npos)
241                     {
242                         continue;
243                     }
244                     sensors[objectPair.first] = interface;
245                 }
246             }
247         }
248     }
249     ManagedObjectType configurations;
250     for (const auto& owner : owners)
251     {
252         // skip if no pid configuration (means probably a sensor)
253         if (!owner.second.first)
254         {
255             continue;
256         }
257         auto endpoint = bus.new_method_call(
258             owner.first.c_str(), owner.second.second.c_str(),
259             "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
260         ManagedObjectType configuration;
261         try
262         {
263             auto responce = bus.call(endpoint);
264             responce.read(configuration);
265         }
266         catch (sdbusplus::exception_t&)
267         {
268             // this shouldn't happen, probably means daemon crashed
269             throw std::runtime_error("Error getting managed objects from " +
270                                      owner.first);
271         }
272 
273         for (auto& pathPair : configuration)
274         {
275             if (pathPair.second.find(pidConfigurationInterface) !=
276                     pathPair.second.end() ||
277                 pathPair.second.find(pidZoneConfigurationInterface) !=
278                     pathPair.second.end() ||
279                 pathPair.second.find(stepwiseConfigurationInterface) !=
280                     pathPair.second.end())
281             {
282                 configurations.emplace(pathPair);
283             }
284         }
285     }
286 
287     // on dbus having an index field is a bit strange, so randomly
288     // assign index based on name property
289     std::vector<std::string> foundZones;
290     for (const auto& configuration : configurations)
291     {
292         auto findZone =
293             configuration.second.find(pidZoneConfigurationInterface);
294         if (findZone != configuration.second.end())
295         {
296             const auto& zone = findZone->second;
297 
298             const std::string& name = std::get<std::string>(zone.at("Name"));
299             size_t index = getZoneIndex(name, foundZones);
300 
301             auto& details = zoneDetailsConfig[index];
302             details.minThermalOutput = std::visit(VariantToDoubleVisitor(),
303                                                   zone.at("MinThermalOutput"));
304             details.failsafePercent = std::visit(VariantToDoubleVisitor(),
305                                                  zone.at("FailSafePercent"));
306         }
307         auto findBase = configuration.second.find(pidConfigurationInterface);
308         if (findBase != configuration.second.end())
309         {
310 
311             const auto& base =
312                 configuration.second.at(pidConfigurationInterface);
313             const std::vector<std::string>& zones =
314                 std::get<std::vector<std::string>>(base.at("Zones"));
315             for (const std::string& zone : zones)
316             {
317                 size_t index = getZoneIndex(zone, foundZones);
318                 conf::PIDConf& conf = zoneConfig[index];
319 
320                 std::vector<std::string> sensorNames =
321                     std::get<std::vector<std::string>>(base.at("Inputs"));
322                 auto findOutputs =
323                     base.find("Outputs"); // currently only fans have outputs
324                 if (findOutputs != base.end())
325                 {
326                     std::vector<std::string> outputs =
327                         std::get<std::vector<std::string>>(findOutputs->second);
328                     sensorNames.insert(sensorNames.end(), outputs.begin(),
329                                        outputs.end());
330                 }
331 
332                 std::vector<std::string> inputs;
333                 std::vector<std::pair<std::string, std::string>>
334                     sensorInterfaces;
335                 for (const std::string& sensorName : sensorNames)
336                 {
337                     std::string name = sensorName;
338                     // replace spaces with underscores to be legal on dbus
339                     std::replace(name.begin(), name.end(), ' ', '_');
340                     findSensors(sensors, name, sensorInterfaces);
341                 }
342 
343                 // if the sensors aren't available in the current state, don't
344                 // add them to the configuration.
345                 if (sensorInterfaces.empty())
346                 {
347                     continue;
348                 }
349                 for (const auto& sensorPathIfacePair : sensorInterfaces)
350                 {
351 
352                     if (sensorPathIfacePair.second == sensorInterface)
353                     {
354                         size_t idx =
355                             sensorPathIfacePair.first.find_last_of("/") + 1;
356                         std::string shortName =
357                             sensorPathIfacePair.first.substr(idx);
358 
359                         inputs.push_back(shortName);
360                         auto& config = sensorConfig[shortName];
361                         config.type = std::get<std::string>(base.at("Class"));
362                         config.readPath = sensorPathIfacePair.first;
363                         // todo: maybe un-hardcode this if we run into slower
364                         // timeouts with sensors
365                         if (config.type == "temp")
366                         {
367                             config.timeout = 0;
368                         }
369                         else if (config.type == "fan")
370                         {
371                             config.max = conf::inheritValueFromDbus;
372                             config.min = conf::inheritValueFromDbus;
373                         }
374                     }
375                     else if (sensorPathIfacePair.second == pwmInterface)
376                     {
377                         // copy so we can modify it
378                         for (std::string otherSensor : sensorNames)
379                         {
380                             std::replace(otherSensor.begin(), otherSensor.end(),
381                                          ' ', '_');
382                             if (sensorPathIfacePair.first.find(otherSensor) !=
383                                 std::string::npos)
384                             {
385                                 continue;
386                             }
387 
388                             auto& config = sensorConfig[otherSensor];
389                             config.writePath = sensorPathIfacePair.first;
390                             // todo: un-hardcode this if there are fans with
391                             // different ranges
392                             config.max = 255;
393                             config.min = 0;
394                         }
395                     }
396                 }
397 
398                 struct conf::ControllerInfo& info =
399                     conf[std::get<std::string>(base.at("Name"))];
400                 info.inputs = std::move(inputs);
401 
402                 info.type = std::get<std::string>(base.at("Class"));
403                 // todo: auto generation yaml -> c script seems to discard this
404                 // value for fans, verify this is okay
405                 if (info.type == "fan")
406                 {
407                     info.setpoint = 0;
408                 }
409                 else
410                 {
411                     info.setpoint = std::visit(VariantToDoubleVisitor(),
412                                                base.at("SetPoint"));
413                 }
414                 info.pidInfo.ts = 1.0; // currently unused
415                 info.pidInfo.proportionalCoeff = std::visit(
416                     VariantToDoubleVisitor(), base.at("PCoefficient"));
417                 info.pidInfo.integralCoeff = std::visit(
418                     VariantToDoubleVisitor(), base.at("ICoefficient"));
419                 info.pidInfo.feedFwdOffset = std::visit(
420                     VariantToDoubleVisitor(), base.at("FFOffCoefficient"));
421                 info.pidInfo.feedFwdGain = std::visit(
422                     VariantToDoubleVisitor(), base.at("FFGainCoefficient"));
423                 info.pidInfo.integralLimit.max =
424                     std::visit(VariantToDoubleVisitor(), base.at("ILimitMax"));
425                 info.pidInfo.integralLimit.min =
426                     std::visit(VariantToDoubleVisitor(), base.at("ILimitMin"));
427                 info.pidInfo.outLim.max = std::visit(VariantToDoubleVisitor(),
428                                                      base.at("OutLimitMax"));
429                 info.pidInfo.outLim.min = std::visit(VariantToDoubleVisitor(),
430                                                      base.at("OutLimitMin"));
431                 info.pidInfo.slewNeg =
432                     std::visit(VariantToDoubleVisitor(), base.at("SlewNeg"));
433                 info.pidInfo.slewPos =
434                     std::visit(VariantToDoubleVisitor(), base.at("SlewPos"));
435                 double negativeHysteresis = 0;
436                 double positiveHysteresis = 0;
437 
438                 auto findNeg = base.find("NegativeHysteresis");
439                 auto findPos = base.find("PositiveHysteresis");
440                 if (findNeg != base.end())
441                 {
442                     negativeHysteresis =
443                         std::visit(VariantToDoubleVisitor(), findNeg->second);
444                 }
445 
446                 if (findPos != base.end())
447                 {
448                     positiveHysteresis =
449                         std::visit(VariantToDoubleVisitor(), findPos->second);
450                 }
451                 info.pidInfo.negativeHysteresis = negativeHysteresis;
452                 info.pidInfo.positiveHysteresis = positiveHysteresis;
453             }
454         }
455         auto findStepwise =
456             configuration.second.find(stepwiseConfigurationInterface);
457         if (findStepwise != configuration.second.end())
458         {
459             const auto& base = findStepwise->second;
460             const std::vector<std::string>& zones =
461                 std::get<std::vector<std::string>>(base.at("Zones"));
462             for (const std::string& zone : zones)
463             {
464                 size_t index = getZoneIndex(zone, foundZones);
465                 conf::PIDConf& conf = zoneConfig[index];
466 
467                 std::vector<std::string> inputs;
468                 std::vector<std::string> sensorNames =
469                     std::get<std::vector<std::string>>(base.at("Inputs"));
470 
471                 bool sensorFound = false;
472                 for (const std::string& sensorName : sensorNames)
473                 {
474                     std::string name = sensorName;
475                     // replace spaces with underscores to be legal on dbus
476                     std::replace(name.begin(), name.end(), ' ', '_');
477                     std::vector<std::pair<std::string, std::string>>
478                         sensorPathIfacePairs;
479 
480                     if (!findSensors(sensors, name, sensorPathIfacePairs))
481                     {
482                         break;
483                     }
484 
485                     for (const auto& sensorPathIfacePair : sensorPathIfacePairs)
486                     {
487                         size_t idx =
488                             sensorPathIfacePair.first.find_last_of("/") + 1;
489                         std::string shortName =
490                             sensorPathIfacePair.first.substr(idx);
491 
492                         inputs.push_back(shortName);
493                         auto& config = sensorConfig[shortName];
494                         config.readPath = sensorPathIfacePair.first;
495                         config.type = "temp";
496                         // todo: maybe un-hardcode this if we run into slower
497                         // timeouts with sensors
498 
499                         config.timeout = 0;
500                         sensorFound = true;
501                     }
502                 }
503                 if (!sensorFound)
504                 {
505                     continue;
506                 }
507                 struct conf::ControllerInfo& info =
508                     conf[std::get<std::string>(base.at("Name"))];
509                 info.inputs = std::move(inputs);
510 
511                 info.type = "stepwise";
512                 info.stepwiseInfo.ts = 1.0; // currently unused
513                 info.stepwiseInfo.positiveHysteresis = 0.0;
514                 info.stepwiseInfo.negativeHysteresis = 0.0;
515                 std::string subtype = std::get<std::string>(base.at("Class"));
516 
517                 info.stepwiseInfo.isCeiling = (subtype == "Ceiling");
518                 auto findPosHyst = base.find("PositiveHysteresis");
519                 auto findNegHyst = base.find("NegativeHysteresis");
520                 if (findPosHyst != base.end())
521                 {
522                     info.stepwiseInfo.positiveHysteresis = std::visit(
523                         VariantToDoubleVisitor(), findPosHyst->second);
524                 }
525                 if (findNegHyst != base.end())
526                 {
527                     info.stepwiseInfo.positiveHysteresis = std::visit(
528                         VariantToDoubleVisitor(), findNegHyst->second);
529                 }
530                 std::vector<double> readings =
531                     std::get<std::vector<double>>(base.at("Reading"));
532                 if (readings.size() > ec::maxStepwisePoints)
533                 {
534                     throw std::invalid_argument("Too many stepwise points.");
535                 }
536                 if (readings.empty())
537                 {
538                     throw std::invalid_argument(
539                         "Must have one stepwise point.");
540                 }
541                 std::copy(readings.begin(), readings.end(),
542                           info.stepwiseInfo.reading);
543                 if (readings.size() < ec::maxStepwisePoints)
544                 {
545                     info.stepwiseInfo.reading[readings.size()] =
546                         std::numeric_limits<double>::quiet_NaN();
547                 }
548                 std::vector<double> outputs =
549                     std::get<std::vector<double>>(base.at("Output"));
550                 if (readings.size() != outputs.size())
551                 {
552                     throw std::invalid_argument(
553                         "Outputs size must match readings");
554                 }
555                 std::copy(outputs.begin(), outputs.end(),
556                           info.stepwiseInfo.output);
557                 if (outputs.size() < ec::maxStepwisePoints)
558                 {
559                     info.stepwiseInfo.output[outputs.size()] =
560                         std::numeric_limits<double>::quiet_NaN();
561                 }
562             }
563         }
564     }
565     if (DEBUG)
566     {
567         debugPrint();
568     }
569     if (zoneConfig.empty() || zoneDetailsConfig.empty())
570     {
571         std::cerr << "No fan zones, application pausing until reboot\n";
572         while (1)
573         {
574             bus.process_discard();
575             bus.wait();
576         }
577     }
578 }
579 } // namespace dbus_configuration
580