xref: /openbmc/dbus-sensors/src/Utils.cpp (revision 6c106d66)
1 /*
2 // Copyright (c) 2017 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 "dbus-sensor_config.h"
18 
19 #include <Utils.hpp>
20 #include <boost/container/flat_map.hpp>
21 #include <sdbusplus/asio/connection.hpp>
22 #include <sdbusplus/asio/object_server.hpp>
23 #include <sdbusplus/bus/match.hpp>
24 
25 #include <filesystem>
26 #include <fstream>
27 #include <memory>
28 #include <regex>
29 #include <stdexcept>
30 #include <string>
31 #include <utility>
32 #include <variant>
33 #include <vector>
34 
35 namespace fs = std::filesystem;
36 
37 static bool powerStatusOn = false;
38 static bool biosHasPost = false;
39 static bool manufacturingMode = false;
40 
41 static std::unique_ptr<sdbusplus::bus::match_t> powerMatch = nullptr;
42 static std::unique_ptr<sdbusplus::bus::match_t> postMatch = nullptr;
43 
44 /**
45  * return the contents of a file
46  * @param[in] hwmonFile - the path to the file to read
47  * @return the contents of the file as a string or nullopt if the file could not
48  * be opened.
49  */
50 
51 std::optional<std::string> openAndRead(const std::string& hwmonFile)
52 {
53     std::string fileVal;
54     std::ifstream fileStream(hwmonFile);
55     if (!fileStream.is_open())
56     {
57         return std::nullopt;
58     }
59     std::getline(fileStream, fileVal);
60     return fileVal;
61 }
62 
63 /**
64  * given a hwmon temperature base name if valid return the full path else
65  * nullopt
66  * @param[in] directory - the hwmon sysfs directory
67  * @param[in] permitSet - a set of labels or hwmon basenames to permit. If this
68  * is empty then *everything* is permitted.
69  * @return a string to the full path of the file to create a temp sensor with or
70  * nullopt to indicate that no sensor should be created for this basename.
71  */
72 std::optional<std::string>
73     getFullHwmonFilePath(const std::string& directory,
74                          const std::string& hwmonBaseName,
75                          const std::set<std::string>& permitSet)
76 {
77     std::optional<std::string> result;
78     std::string filename;
79     if (permitSet.empty())
80     {
81         result = directory + "/" + hwmonBaseName + "_input";
82         return result;
83     }
84     filename = directory + "/" + hwmonBaseName + "_label";
85     auto searchVal = openAndRead(filename);
86     if (!searchVal)
87     {
88         /* if the hwmon temp doesn't have a corresponding label file
89          * then use the hwmon temperature base name
90          */
91         searchVal = hwmonBaseName;
92     }
93     if (permitSet.find(*searchVal) != permitSet.end())
94     {
95         result = directory + "/" + hwmonBaseName + "_input";
96     }
97     return result;
98 }
99 
100 /**
101  * retrieve a set of basenames and labels to allow sensor creation for.
102  * @param[in] config - a map representing the configuration for a specific
103  * device
104  * @return a set of basenames and labels to allow sensor creation for. An empty
105  * set indicates that everything is permitted.
106  */
107 std::set<std::string> getPermitSet(const SensorBaseConfigMap& config)
108 {
109     auto permitAttribute = config.find("Labels");
110     std::set<std::string> permitSet;
111     if (permitAttribute != config.end())
112     {
113         try
114         {
115             auto val =
116                 std::get<std::vector<std::string>>(permitAttribute->second);
117 
118             permitSet.insert(std::make_move_iterator(val.begin()),
119                              std::make_move_iterator(val.end()));
120         }
121         catch (const std::bad_variant_access& err)
122         {
123             std::cerr << err.what()
124                       << ":PermitList does not contain a list, wrong "
125                          "variant type.\n";
126         }
127     }
128     return permitSet;
129 }
130 
131 bool getSensorConfiguration(
132     const std::string& type,
133     const std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
134     ManagedObjectType& resp)
135 {
136     return getSensorConfiguration(type, dbusConnection, resp, false);
137 }
138 
139 bool getSensorConfiguration(
140     const std::string& type,
141     const std::shared_ptr<sdbusplus::asio::connection>& dbusConnection,
142     ManagedObjectType& resp, bool useCache)
143 {
144     static ManagedObjectType managedObj;
145 
146     if (!useCache)
147     {
148         managedObj.clear();
149         sdbusplus::message_t getManagedObjects =
150             dbusConnection->new_method_call(
151                 entityManagerName, "/", "org.freedesktop.DBus.ObjectManager",
152                 "GetManagedObjects");
153         bool err = false;
154         try
155         {
156             sdbusplus::message_t reply =
157                 dbusConnection->call(getManagedObjects);
158             reply.read(managedObj);
159         }
160         catch (const sdbusplus::exception_t& e)
161         {
162             std::cerr << "While calling GetManagedObjects on service:"
163                       << entityManagerName << " exception name:" << e.name()
164                       << "and description:" << e.description()
165                       << " was thrown\n";
166             err = true;
167         }
168 
169         if (err)
170         {
171             std::cerr << "Error communicating to entity manager\n";
172             return false;
173         }
174     }
175     for (const auto& pathPair : managedObj)
176     {
177         bool correctType = false;
178         for (const auto& [intf, cfg] : pathPair.second)
179         {
180             if (intf.starts_with(type))
181             {
182                 correctType = true;
183                 break;
184             }
185         }
186         if (correctType)
187         {
188             resp.emplace(pathPair);
189         }
190     }
191     return true;
192 }
193 
194 bool findFiles(const fs::path& dirPath, std::string_view matchString,
195                std::vector<fs::path>& foundPaths, int symlinkDepth)
196 {
197     std::error_code ec;
198     if (!fs::exists(dirPath, ec))
199     {
200         return false;
201     }
202 
203     std::vector<std::regex> matchPieces;
204 
205     size_t pos = 0;
206     std::string token;
207     // Generate the regex expressions list from the match we were given
208     while ((pos = matchString.find('/')) != std::string::npos)
209     {
210         token = matchString.substr(0, pos);
211         matchPieces.emplace_back(token);
212         matchString.remove_prefix(pos + 1);
213     }
214     matchPieces.emplace_back(std::string{matchString});
215 
216     // Check if the match string contains directories, and skip the match of
217     // subdirectory if not
218     if (matchPieces.size() <= 1)
219     {
220         std::regex search(std::string{matchString});
221         std::smatch match;
222         for (auto p = fs::recursive_directory_iterator(
223                  dirPath, fs::directory_options::follow_directory_symlink);
224              p != fs::recursive_directory_iterator(); ++p)
225         {
226             std::string path = p->path().string();
227             if (!is_directory(*p))
228             {
229                 if (std::regex_search(path, match, search))
230                 {
231                     foundPaths.emplace_back(p->path());
232                 }
233             }
234             if (p.depth() >= symlinkDepth)
235             {
236                 p.disable_recursion_pending();
237             }
238         }
239         return true;
240     }
241 
242     // The match string contains directories, verify each level of sub
243     // directories
244     for (auto p = fs::recursive_directory_iterator(
245              dirPath, fs::directory_options::follow_directory_symlink);
246          p != fs::recursive_directory_iterator(); ++p)
247     {
248         std::vector<std::regex>::iterator matchPiece = matchPieces.begin();
249         fs::path::iterator pathIt = p->path().begin();
250         for (const fs::path& dir : dirPath)
251         {
252             if (dir.empty())
253             {
254                 // When the path ends with '/', it gets am empty path
255                 // skip such case.
256                 break;
257             }
258             pathIt++;
259         }
260 
261         while (pathIt != p->path().end())
262         {
263             // Found a path deeper than match.
264             if (matchPiece == matchPieces.end())
265             {
266                 p.disable_recursion_pending();
267                 break;
268             }
269             std::smatch match;
270             std::string component = pathIt->string();
271             std::regex regexPiece(*matchPiece);
272             if (!std::regex_match(component, match, regexPiece))
273             {
274                 // path prefix doesn't match, no need to iterate further
275                 p.disable_recursion_pending();
276                 break;
277             }
278             matchPiece++;
279             pathIt++;
280         }
281 
282         if (!is_directory(*p))
283         {
284             if (matchPiece == matchPieces.end())
285             {
286                 foundPaths.emplace_back(p->path());
287             }
288         }
289 
290         if (p.depth() >= symlinkDepth)
291         {
292             p.disable_recursion_pending();
293         }
294     }
295     return true;
296 }
297 
298 bool isPowerOn(void)
299 {
300     if (!powerMatch)
301     {
302         throw std::runtime_error("Power Match Not Created");
303     }
304     return powerStatusOn;
305 }
306 
307 bool hasBiosPost(void)
308 {
309     if (!postMatch)
310     {
311         throw std::runtime_error("Post Match Not Created");
312     }
313     return biosHasPost;
314 }
315 
316 bool readingStateGood(const PowerState& powerState)
317 {
318     if (powerState == PowerState::on && !isPowerOn())
319     {
320         return false;
321     }
322     if (powerState == PowerState::biosPost && (!hasBiosPost() || !isPowerOn()))
323     {
324         return false;
325     }
326 
327     return true;
328 }
329 
330 static void
331     getPowerStatus(const std::shared_ptr<sdbusplus::asio::connection>& conn,
332                    size_t retries = 2)
333 {
334     conn->async_method_call(
335         [conn, retries](boost::system::error_code ec,
336                         const std::variant<std::string>& state) {
337         if (ec)
338         {
339             if (retries != 0U)
340             {
341                 auto timer = std::make_shared<boost::asio::steady_timer>(
342                     conn->get_io_context());
343                 timer->expires_after(std::chrono::seconds(15));
344                 timer->async_wait(
345                     [timer, conn, retries](boost::system::error_code) {
346                     getPowerStatus(conn, retries - 1);
347                 });
348                 return;
349             }
350 
351             // we commonly come up before power control, we'll capture the
352             // property change later
353             std::cerr << "error getting power status " << ec.message() << "\n";
354             return;
355         }
356         powerStatusOn = std::get<std::string>(state).ends_with(".Running");
357         },
358         power::busname, power::path, properties::interface, properties::get,
359         power::interface, power::property);
360 }
361 
362 static void
363     getPostStatus(const std::shared_ptr<sdbusplus::asio::connection>& conn,
364                   size_t retries = 2)
365 {
366     conn->async_method_call(
367         [conn, retries](boost::system::error_code ec,
368                         const std::variant<std::string>& state) {
369         if (ec)
370         {
371             if (retries != 0U)
372             {
373                 auto timer = std::make_shared<boost::asio::steady_timer>(
374                     conn->get_io_context());
375                 timer->expires_after(std::chrono::seconds(15));
376                 timer->async_wait(
377                     [timer, conn, retries](boost::system::error_code) {
378                     getPostStatus(conn, retries - 1);
379                 });
380                 return;
381             }
382             // we commonly come up before power control, we'll capture the
383             // property change later
384             std::cerr << "error getting post status " << ec.message() << "\n";
385             return;
386         }
387         const auto& value = std::get<std::string>(state);
388         biosHasPost = (value != "Inactive") &&
389                       (value != "xyz.openbmc_project.State.OperatingSystem."
390                                 "Status.OSStatus.Inactive");
391         },
392         post::busname, post::path, properties::interface, properties::get,
393         post::interface, post::property);
394 }
395 
396 void setupPowerMatchCallback(
397     const std::shared_ptr<sdbusplus::asio::connection>& conn,
398     std::function<void(PowerState type, bool state)>&& hostStatusCallback)
399 {
400     static boost::asio::steady_timer timer(conn->get_io_context());
401     // create a match for powergood changes, first time do a method call to
402     // cache the correct value
403     if (powerMatch)
404     {
405         return;
406     }
407 
408     powerMatch = std::make_unique<sdbusplus::bus::match_t>(
409         static_cast<sdbusplus::bus_t&>(*conn),
410         "type='signal',interface='" + std::string(properties::interface) +
411             "',path='" + std::string(power::path) + "',arg0='" +
412             std::string(power::interface) + "'",
413         [hostStatusCallback](sdbusplus::message_t& message) {
414         std::string objectName;
415         boost::container::flat_map<std::string, std::variant<std::string>>
416             values;
417         message.read(objectName, values);
418         auto findState = values.find(power::property);
419         if (findState != values.end())
420         {
421             bool on =
422                 std::get<std::string>(findState->second).ends_with(".Running");
423             if (!on)
424             {
425                 timer.cancel();
426                 powerStatusOn = false;
427                 hostStatusCallback(PowerState::on, powerStatusOn);
428                 return;
429             }
430             // on comes too quickly
431             timer.expires_after(std::chrono::seconds(10));
432             timer.async_wait(
433                 [hostStatusCallback](boost::system::error_code ec) {
434                 if (ec == boost::asio::error::operation_aborted)
435                 {
436                     return;
437                 }
438                 if (ec)
439                 {
440                     std::cerr << "Timer error " << ec.message() << "\n";
441                     return;
442                 }
443                 powerStatusOn = true;
444                 hostStatusCallback(PowerState::on, powerStatusOn);
445             });
446         }
447         });
448 
449     postMatch = std::make_unique<sdbusplus::bus::match_t>(
450         static_cast<sdbusplus::bus_t&>(*conn),
451         "type='signal',interface='" + std::string(properties::interface) +
452             "',path='" + std::string(post::path) + "',arg0='" +
453             std::string(post::interface) + "'",
454         [hostStatusCallback](sdbusplus::message_t& message) {
455         std::string objectName;
456         boost::container::flat_map<std::string, std::variant<std::string>>
457             values;
458         message.read(objectName, values);
459         auto findState = values.find(post::property);
460         if (findState != values.end())
461         {
462             auto& value = std::get<std::string>(findState->second);
463             biosHasPost = (value != "Inactive") &&
464                           (value != "xyz.openbmc_project.State.OperatingSystem."
465                                     "Status.OSStatus.Inactive");
466             hostStatusCallback(PowerState::biosPost, biosHasPost);
467         }
468         });
469 
470     getPowerStatus(conn);
471     getPostStatus(conn);
472 }
473 
474 void setupPowerMatch(const std::shared_ptr<sdbusplus::asio::connection>& conn)
475 {
476     setupPowerMatchCallback(conn, [](PowerState, bool) {});
477 }
478 
479 // replaces limits if MinReading and MaxReading are found.
480 void findLimits(std::pair<double, double>& limits,
481                 const SensorBaseConfiguration* data)
482 {
483     if (data == nullptr)
484     {
485         return;
486     }
487     auto maxFind = data->second.find("MaxReading");
488     auto minFind = data->second.find("MinReading");
489 
490     if (minFind != data->second.end())
491     {
492         limits.first = std::visit(VariantToDoubleVisitor(), minFind->second);
493     }
494     if (maxFind != data->second.end())
495     {
496         limits.second = std::visit(VariantToDoubleVisitor(), maxFind->second);
497     }
498 }
499 
500 void createAssociation(
501     std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
502     const std::string& path)
503 {
504     if (association)
505     {
506         fs::path p(path);
507 
508         std::vector<Association> associations;
509         associations.emplace_back("chassis", "all_sensors",
510                                   p.parent_path().string());
511         association->register_property("Associations", associations);
512         association->initialize();
513     }
514 }
515 
516 void setInventoryAssociation(
517     const std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
518     const std::string& path,
519     const std::vector<std::string>& chassisPaths = std::vector<std::string>())
520 {
521     if (association)
522     {
523         fs::path p(path);
524         std::vector<Association> associations;
525         std::string objPath(p.parent_path().string());
526 
527         associations.emplace_back("inventory", "sensors", objPath);
528         associations.emplace_back("chassis", "all_sensors", objPath);
529 
530         for (const std::string& chassisPath : chassisPaths)
531         {
532             associations.emplace_back("chassis", "all_sensors", chassisPath);
533         }
534 
535         association->register_property("Associations", associations);
536         association->initialize();
537     }
538 }
539 
540 void createInventoryAssoc(
541     const std::shared_ptr<sdbusplus::asio::connection>& conn,
542     const std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
543     const std::string& path)
544 {
545     if (!association)
546     {
547         return;
548     }
549 
550     conn->async_method_call(
551         [association, path](const boost::system::error_code ec,
552                             const std::vector<std::string>& invSysObjPaths) {
553         if (ec)
554         {
555             // In case of error, set the default associations and
556             // initialize the association Interface.
557             setInventoryAssociation(association, path);
558             return;
559         }
560         setInventoryAssociation(association, path, invSysObjPaths);
561         },
562         mapper::busName, mapper::path, mapper::interface, "GetSubTreePaths",
563         "/xyz/openbmc_project/inventory/system", 2,
564         std::array<std::string, 1>{
565             "xyz.openbmc_project.Inventory.Item.System"});
566 }
567 
568 std::optional<double> readFile(const std::string& thresholdFile,
569                                const double& scaleFactor)
570 {
571     std::string line;
572     std::ifstream labelFile(thresholdFile);
573     if (labelFile.good())
574     {
575         std::getline(labelFile, line);
576         labelFile.close();
577 
578         try
579         {
580             return std::stod(line) / scaleFactor;
581         }
582         catch (const std::invalid_argument&)
583         {
584             return std::nullopt;
585         }
586     }
587     return std::nullopt;
588 }
589 
590 std::optional<std::tuple<std::string, std::string, std::string>>
591     splitFileName(const fs::path& filePath)
592 {
593     if (filePath.has_filename())
594     {
595         const auto fileName = filePath.filename().string();
596 
597         size_t numberPos = std::strcspn(fileName.c_str(), "1234567890");
598         size_t itemPos = std::strcspn(fileName.c_str(), "_");
599 
600         if (numberPos > 0 && itemPos > numberPos && fileName.size() > itemPos)
601         {
602             return std::make_optional(
603                 std::make_tuple(fileName.substr(0, numberPos),
604                                 fileName.substr(numberPos, itemPos - numberPos),
605                                 fileName.substr(itemPos + 1, fileName.size())));
606         }
607     }
608     return std::nullopt;
609 }
610 
611 static void handleSpecialModeChange(const std::string& manufacturingModeStatus)
612 {
613     manufacturingMode = false;
614     if (manufacturingModeStatus == "xyz.openbmc_project.Control.Security."
615                                    "SpecialMode.Modes.Manufacturing")
616     {
617         manufacturingMode = true;
618     }
619     if (validateUnsecureFeature == 1)
620     {
621         if (manufacturingModeStatus == "xyz.openbmc_project.Control.Security."
622                                        "SpecialMode.Modes.ValidationUnsecure")
623         {
624             manufacturingMode = true;
625         }
626     }
627 }
628 
629 void setupManufacturingModeMatch(sdbusplus::asio::connection& conn)
630 {
631     namespace rules = sdbusplus::bus::match::rules;
632     static constexpr const char* specialModeInterface =
633         "xyz.openbmc_project.Security.SpecialMode";
634 
635     const std::string filterSpecialModeIntfAdd =
636         rules::interfacesAdded() +
637         rules::argNpath(0, "/xyz/openbmc_project/security/special_mode");
638     static std::unique_ptr<sdbusplus::bus::match_t> specialModeIntfMatch =
639         std::make_unique<sdbusplus::bus::match_t>(conn,
640                                                   filterSpecialModeIntfAdd,
641                                                   [](sdbusplus::message_t& m) {
642         sdbusplus::message::object_path path;
643         using PropertyMap =
644             boost::container::flat_map<std::string, std::variant<std::string>>;
645         boost::container::flat_map<std::string, PropertyMap> interfaceAdded;
646         m.read(path, interfaceAdded);
647         auto intfItr = interfaceAdded.find(specialModeInterface);
648         if (intfItr == interfaceAdded.end())
649         {
650             return;
651         }
652         PropertyMap& propertyList = intfItr->second;
653         auto itr = propertyList.find("SpecialMode");
654         if (itr == propertyList.end())
655         {
656             std::cerr << "error getting  SpecialMode property "
657                       << "\n";
658             return;
659         }
660         auto* manufacturingModeStatus = std::get_if<std::string>(&itr->second);
661         handleSpecialModeChange(*manufacturingModeStatus);
662         });
663 
664     const std::string filterSpecialModeChange =
665         rules::type::signal() + rules::member("PropertiesChanged") +
666         rules::interface("org.freedesktop.DBus.Properties") +
667         rules::argN(0, specialModeInterface);
668     static std::unique_ptr<sdbusplus::bus::match_t> specialModeChangeMatch =
669         std::make_unique<sdbusplus::bus::match_t>(conn, filterSpecialModeChange,
670                                                   [](sdbusplus::message_t& m) {
671         std::string interfaceName;
672         boost::container::flat_map<std::string, std::variant<std::string>>
673             propertiesChanged;
674 
675         m.read(interfaceName, propertiesChanged);
676         auto itr = propertiesChanged.find("SpecialMode");
677         if (itr == propertiesChanged.end())
678         {
679             return;
680         }
681         auto* manufacturingModeStatus = std::get_if<std::string>(&itr->second);
682         handleSpecialModeChange(*manufacturingModeStatus);
683         });
684 
685     conn.async_method_call(
686         [](const boost::system::error_code ec,
687            const std::variant<std::string>& getManufactMode) {
688         if (ec)
689         {
690             std::cerr << "error getting  SpecialMode status " << ec.message()
691                       << "\n";
692             return;
693         }
694         const auto* manufacturingModeStatus =
695             std::get_if<std::string>(&getManufactMode);
696         handleSpecialModeChange(*manufacturingModeStatus);
697         },
698         "xyz.openbmc_project.SpecialMode",
699         "/xyz/openbmc_project/security/special_mode",
700         "org.freedesktop.DBus.Properties", "Get", specialModeInterface,
701         "SpecialMode");
702 }
703 
704 bool getManufacturingMode()
705 {
706     return manufacturingMode;
707 }
708 
709 std::vector<std::unique_ptr<sdbusplus::bus::match_t>>
710     setupPropertiesChangedMatches(
711         sdbusplus::asio::connection& bus, std::span<const char* const> types,
712         const std::function<void(sdbusplus::message_t&)>& handler)
713 {
714     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches;
715     for (const char* type : types)
716     {
717         auto match = std::make_unique<sdbusplus::bus::match_t>(
718             static_cast<sdbusplus::bus_t&>(bus),
719             "type='signal',member='PropertiesChanged',path_namespace='" +
720                 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
721             handler);
722         matches.emplace_back(std::move(match));
723     }
724     return matches;
725 }
726