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