xref: /openbmc/dbus-sensors/src/Utils.cpp (revision 79d8aef4)
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         try
154         {
155             sdbusplus::message_t reply =
156                 dbusConnection->call(getManagedObjects);
157             reply.read(managedObj);
158         }
159         catch (const sdbusplus::exception_t& e)
160         {
161             std::cerr << "While calling GetManagedObjects on service:"
162                       << entityManagerName << " exception name:" << e.name()
163                       << "and description:" << e.description()
164                       << " was thrown\n";
165             return false;
166         }
167     }
168     for (const auto& pathPair : managedObj)
169     {
170         for (const auto& [intf, cfg] : pathPair.second)
171         {
172             if (intf.starts_with(type))
173             {
174                 resp.emplace(pathPair);
175                 break;
176             }
177         }
178     }
179     return true;
180 }
181 
182 bool findFiles(const fs::path& dirPath, std::string_view matchString,
183                std::vector<fs::path>& foundPaths, int symlinkDepth)
184 {
185     std::error_code ec;
186     if (!fs::exists(dirPath, ec))
187     {
188         return false;
189     }
190 
191     std::vector<std::regex> matchPieces;
192 
193     size_t pos = 0;
194     std::string token;
195     // Generate the regex expressions list from the match we were given
196     while ((pos = matchString.find('/')) != std::string::npos)
197     {
198         token = matchString.substr(0, pos);
199         matchPieces.emplace_back(token);
200         matchString.remove_prefix(pos + 1);
201     }
202     matchPieces.emplace_back(std::string{matchString});
203 
204     // Check if the match string contains directories, and skip the match of
205     // subdirectory if not
206     if (matchPieces.size() <= 1)
207     {
208         std::regex search(std::string{matchString});
209         std::smatch match;
210         for (auto p = fs::recursive_directory_iterator(
211                  dirPath, fs::directory_options::follow_directory_symlink);
212              p != fs::recursive_directory_iterator(); ++p)
213         {
214             std::string path = p->path().string();
215             if (!is_directory(*p))
216             {
217                 if (std::regex_search(path, match, search))
218                 {
219                     foundPaths.emplace_back(p->path());
220                 }
221             }
222             if (p.depth() >= symlinkDepth)
223             {
224                 p.disable_recursion_pending();
225             }
226         }
227         return true;
228     }
229 
230     // The match string contains directories, verify each level of sub
231     // directories
232     for (auto p = fs::recursive_directory_iterator(
233              dirPath, fs::directory_options::follow_directory_symlink);
234          p != fs::recursive_directory_iterator(); ++p)
235     {
236         std::vector<std::regex>::iterator matchPiece = matchPieces.begin();
237         fs::path::iterator pathIt = p->path().begin();
238         for (const fs::path& dir : dirPath)
239         {
240             if (dir.empty())
241             {
242                 // When the path ends with '/', it gets am empty path
243                 // skip such case.
244                 break;
245             }
246             pathIt++;
247         }
248 
249         while (pathIt != p->path().end())
250         {
251             // Found a path deeper than match.
252             if (matchPiece == matchPieces.end())
253             {
254                 p.disable_recursion_pending();
255                 break;
256             }
257             std::smatch match;
258             std::string component = pathIt->string();
259             std::regex regexPiece(*matchPiece);
260             if (!std::regex_match(component, match, regexPiece))
261             {
262                 // path prefix doesn't match, no need to iterate further
263                 p.disable_recursion_pending();
264                 break;
265             }
266             matchPiece++;
267             pathIt++;
268         }
269 
270         if (!is_directory(*p))
271         {
272             if (matchPiece == matchPieces.end())
273             {
274                 foundPaths.emplace_back(p->path());
275             }
276         }
277 
278         if (p.depth() >= symlinkDepth)
279         {
280             p.disable_recursion_pending();
281         }
282     }
283     return true;
284 }
285 
286 bool isPowerOn(void)
287 {
288     if (!powerMatch)
289     {
290         throw std::runtime_error("Power Match Not Created");
291     }
292     return powerStatusOn;
293 }
294 
295 bool hasBiosPost(void)
296 {
297     if (!postMatch)
298     {
299         throw std::runtime_error("Post Match Not Created");
300     }
301     return biosHasPost;
302 }
303 
304 bool readingStateGood(const PowerState& powerState)
305 {
306     if (powerState == PowerState::on && !isPowerOn())
307     {
308         return false;
309     }
310     if (powerState == PowerState::biosPost && (!hasBiosPost() || !isPowerOn()))
311     {
312         return false;
313     }
314 
315     return true;
316 }
317 
318 static void
319     getPowerStatus(const std::shared_ptr<sdbusplus::asio::connection>& conn,
320                    size_t retries = 2)
321 {
322     conn->async_method_call(
323         [conn, retries](boost::system::error_code ec,
324                         const std::variant<std::string>& state) {
325         if (ec)
326         {
327             if (retries != 0U)
328             {
329                 auto timer = std::make_shared<boost::asio::steady_timer>(
330                     conn->get_io_context());
331                 timer->expires_after(std::chrono::seconds(15));
332                 timer->async_wait(
333                     [timer, conn, retries](boost::system::error_code) {
334                     getPowerStatus(conn, retries - 1);
335                 });
336                 return;
337             }
338 
339             // we commonly come up before power control, we'll capture the
340             // property change later
341             std::cerr << "error getting power status " << ec.message() << "\n";
342             return;
343         }
344         powerStatusOn = std::get<std::string>(state).ends_with(".Running");
345         },
346         power::busname, power::path, properties::interface, properties::get,
347         power::interface, power::property);
348 }
349 
350 static void
351     getPostStatus(const std::shared_ptr<sdbusplus::asio::connection>& conn,
352                   size_t retries = 2)
353 {
354     conn->async_method_call(
355         [conn, retries](boost::system::error_code ec,
356                         const std::variant<std::string>& state) {
357         if (ec)
358         {
359             if (retries != 0U)
360             {
361                 auto timer = std::make_shared<boost::asio::steady_timer>(
362                     conn->get_io_context());
363                 timer->expires_after(std::chrono::seconds(15));
364                 timer->async_wait(
365                     [timer, conn, retries](boost::system::error_code) {
366                     getPostStatus(conn, retries - 1);
367                 });
368                 return;
369             }
370             // we commonly come up before power control, we'll capture the
371             // property change later
372             std::cerr << "error getting post status " << ec.message() << "\n";
373             return;
374         }
375         const auto& value = std::get<std::string>(state);
376         biosHasPost = (value != "Inactive") &&
377                       (value != "xyz.openbmc_project.State.OperatingSystem."
378                                 "Status.OSStatus.Inactive");
379         },
380         post::busname, post::path, properties::interface, properties::get,
381         post::interface, post::property);
382 }
383 
384 void setupPowerMatchCallback(
385     const std::shared_ptr<sdbusplus::asio::connection>& conn,
386     std::function<void(PowerState type, bool state)>&& hostStatusCallback)
387 {
388     static boost::asio::steady_timer timer(conn->get_io_context());
389     // create a match for powergood changes, first time do a method call to
390     // cache the correct value
391     if (powerMatch)
392     {
393         return;
394     }
395 
396     powerMatch = std::make_unique<sdbusplus::bus::match_t>(
397         static_cast<sdbusplus::bus_t&>(*conn),
398         "type='signal',interface='" + std::string(properties::interface) +
399             "',path='" + std::string(power::path) + "',arg0='" +
400             std::string(power::interface) + "'",
401         [hostStatusCallback](sdbusplus::message_t& message) {
402         std::string objectName;
403         boost::container::flat_map<std::string, std::variant<std::string>>
404             values;
405         message.read(objectName, values);
406         auto findState = values.find(power::property);
407         if (findState != values.end())
408         {
409             bool on =
410                 std::get<std::string>(findState->second).ends_with(".Running");
411             if (!on)
412             {
413                 timer.cancel();
414                 powerStatusOn = false;
415                 hostStatusCallback(PowerState::on, powerStatusOn);
416                 return;
417             }
418             // on comes too quickly
419             timer.expires_after(std::chrono::seconds(10));
420             timer.async_wait(
421                 [hostStatusCallback](boost::system::error_code ec) {
422                 if (ec == boost::asio::error::operation_aborted)
423                 {
424                     return;
425                 }
426                 if (ec)
427                 {
428                     std::cerr << "Timer error " << ec.message() << "\n";
429                     return;
430                 }
431                 powerStatusOn = true;
432                 hostStatusCallback(PowerState::on, powerStatusOn);
433             });
434         }
435         });
436 
437     postMatch = std::make_unique<sdbusplus::bus::match_t>(
438         static_cast<sdbusplus::bus_t&>(*conn),
439         "type='signal',interface='" + std::string(properties::interface) +
440             "',path='" + std::string(post::path) + "',arg0='" +
441             std::string(post::interface) + "'",
442         [hostStatusCallback](sdbusplus::message_t& message) {
443         std::string objectName;
444         boost::container::flat_map<std::string, std::variant<std::string>>
445             values;
446         message.read(objectName, values);
447         auto findState = values.find(post::property);
448         if (findState != values.end())
449         {
450             auto& value = std::get<std::string>(findState->second);
451             biosHasPost = (value != "Inactive") &&
452                           (value != "xyz.openbmc_project.State.OperatingSystem."
453                                     "Status.OSStatus.Inactive");
454             hostStatusCallback(PowerState::biosPost, biosHasPost);
455         }
456         });
457 
458     getPowerStatus(conn);
459     getPostStatus(conn);
460 }
461 
462 void setupPowerMatch(const std::shared_ptr<sdbusplus::asio::connection>& conn)
463 {
464     setupPowerMatchCallback(conn, [](PowerState, bool) {});
465 }
466 
467 // replaces limits if MinReading and MaxReading are found.
468 void findLimits(std::pair<double, double>& limits,
469                 const SensorBaseConfiguration* data)
470 {
471     if (data == nullptr)
472     {
473         return;
474     }
475     auto maxFind = data->second.find("MaxReading");
476     auto minFind = data->second.find("MinReading");
477 
478     if (minFind != data->second.end())
479     {
480         limits.first = std::visit(VariantToDoubleVisitor(), minFind->second);
481     }
482     if (maxFind != data->second.end())
483     {
484         limits.second = std::visit(VariantToDoubleVisitor(), maxFind->second);
485     }
486 }
487 
488 void createAssociation(
489     std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
490     const std::string& path)
491 {
492     if (association)
493     {
494         fs::path p(path);
495 
496         std::vector<Association> associations;
497         associations.emplace_back("chassis", "all_sensors",
498                                   p.parent_path().string());
499         association->register_property("Associations", associations);
500         association->initialize();
501     }
502 }
503 
504 void setInventoryAssociation(
505     const std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
506     const std::string& path,
507     const std::vector<std::string>& chassisPaths = std::vector<std::string>())
508 {
509     if (association)
510     {
511         fs::path p(path);
512         std::vector<Association> associations;
513         std::string objPath(p.parent_path().string());
514 
515         associations.emplace_back("inventory", "sensors", objPath);
516         associations.emplace_back("chassis", "all_sensors", objPath);
517 
518         for (const std::string& chassisPath : chassisPaths)
519         {
520             associations.emplace_back("chassis", "all_sensors", chassisPath);
521         }
522 
523         association->register_property("Associations", associations);
524         association->initialize();
525     }
526 }
527 
528 void createInventoryAssoc(
529     const std::shared_ptr<sdbusplus::asio::connection>& conn,
530     const std::shared_ptr<sdbusplus::asio::dbus_interface>& association,
531     const std::string& path)
532 {
533     if (!association)
534     {
535         return;
536     }
537 
538     conn->async_method_call(
539         [association, path](const boost::system::error_code ec,
540                             const std::vector<std::string>& invSysObjPaths) {
541         if (ec)
542         {
543             // In case of error, set the default associations and
544             // initialize the association Interface.
545             setInventoryAssociation(association, path);
546             return;
547         }
548         setInventoryAssociation(association, path, invSysObjPaths);
549         },
550         mapper::busName, mapper::path, mapper::interface, "GetSubTreePaths",
551         "/xyz/openbmc_project/inventory/system", 2,
552         std::array<std::string, 1>{
553             "xyz.openbmc_project.Inventory.Item.System"});
554 }
555 
556 std::optional<double> readFile(const std::string& thresholdFile,
557                                const double& scaleFactor)
558 {
559     std::string line;
560     std::ifstream labelFile(thresholdFile);
561     if (labelFile.good())
562     {
563         std::getline(labelFile, line);
564         labelFile.close();
565 
566         try
567         {
568             return std::stod(line) / scaleFactor;
569         }
570         catch (const std::invalid_argument&)
571         {
572             return std::nullopt;
573         }
574     }
575     return std::nullopt;
576 }
577 
578 std::optional<std::tuple<std::string, std::string, std::string>>
579     splitFileName(const fs::path& filePath)
580 {
581     if (filePath.has_filename())
582     {
583         const auto fileName = filePath.filename().string();
584 
585         size_t numberPos = std::strcspn(fileName.c_str(), "1234567890");
586         size_t itemPos = std::strcspn(fileName.c_str(), "_");
587 
588         if (numberPos > 0 && itemPos > numberPos && fileName.size() > itemPos)
589         {
590             return std::make_optional(
591                 std::make_tuple(fileName.substr(0, numberPos),
592                                 fileName.substr(numberPos, itemPos - numberPos),
593                                 fileName.substr(itemPos + 1, fileName.size())));
594         }
595     }
596     return std::nullopt;
597 }
598 
599 static void handleSpecialModeChange(const std::string& manufacturingModeStatus)
600 {
601     manufacturingMode = false;
602     if (manufacturingModeStatus == "xyz.openbmc_project.Control.Security."
603                                    "SpecialMode.Modes.Manufacturing")
604     {
605         manufacturingMode = true;
606     }
607     if (validateUnsecureFeature == 1)
608     {
609         if (manufacturingModeStatus == "xyz.openbmc_project.Control.Security."
610                                        "SpecialMode.Modes.ValidationUnsecure")
611         {
612             manufacturingMode = true;
613         }
614     }
615 }
616 
617 void setupManufacturingModeMatch(sdbusplus::asio::connection& conn)
618 {
619     namespace rules = sdbusplus::bus::match::rules;
620     static constexpr const char* specialModeInterface =
621         "xyz.openbmc_project.Security.SpecialMode";
622 
623     const std::string filterSpecialModeIntfAdd =
624         rules::interfacesAdded() +
625         rules::argNpath(0, "/xyz/openbmc_project/security/special_mode");
626     static std::unique_ptr<sdbusplus::bus::match_t> specialModeIntfMatch =
627         std::make_unique<sdbusplus::bus::match_t>(conn,
628                                                   filterSpecialModeIntfAdd,
629                                                   [](sdbusplus::message_t& m) {
630         sdbusplus::message::object_path path;
631         using PropertyMap =
632             boost::container::flat_map<std::string, std::variant<std::string>>;
633         boost::container::flat_map<std::string, PropertyMap> interfaceAdded;
634         m.read(path, interfaceAdded);
635         auto intfItr = interfaceAdded.find(specialModeInterface);
636         if (intfItr == interfaceAdded.end())
637         {
638             return;
639         }
640         PropertyMap& propertyList = intfItr->second;
641         auto itr = propertyList.find("SpecialMode");
642         if (itr == propertyList.end())
643         {
644             std::cerr << "error getting  SpecialMode property "
645                       << "\n";
646             return;
647         }
648         auto* manufacturingModeStatus = std::get_if<std::string>(&itr->second);
649         handleSpecialModeChange(*manufacturingModeStatus);
650         });
651 
652     const std::string filterSpecialModeChange =
653         rules::type::signal() + rules::member("PropertiesChanged") +
654         rules::interface("org.freedesktop.DBus.Properties") +
655         rules::argN(0, specialModeInterface);
656     static std::unique_ptr<sdbusplus::bus::match_t> specialModeChangeMatch =
657         std::make_unique<sdbusplus::bus::match_t>(conn, filterSpecialModeChange,
658                                                   [](sdbusplus::message_t& m) {
659         std::string interfaceName;
660         boost::container::flat_map<std::string, std::variant<std::string>>
661             propertiesChanged;
662 
663         m.read(interfaceName, propertiesChanged);
664         auto itr = propertiesChanged.find("SpecialMode");
665         if (itr == propertiesChanged.end())
666         {
667             return;
668         }
669         auto* manufacturingModeStatus = std::get_if<std::string>(&itr->second);
670         handleSpecialModeChange(*manufacturingModeStatus);
671         });
672 
673     conn.async_method_call(
674         [](const boost::system::error_code ec,
675            const std::variant<std::string>& getManufactMode) {
676         if (ec)
677         {
678             std::cerr << "error getting  SpecialMode status " << ec.message()
679                       << "\n";
680             return;
681         }
682         const auto* manufacturingModeStatus =
683             std::get_if<std::string>(&getManufactMode);
684         handleSpecialModeChange(*manufacturingModeStatus);
685         },
686         "xyz.openbmc_project.SpecialMode",
687         "/xyz/openbmc_project/security/special_mode",
688         "org.freedesktop.DBus.Properties", "Get", specialModeInterface,
689         "SpecialMode");
690 }
691 
692 bool getManufacturingMode()
693 {
694     return manufacturingMode;
695 }
696 
697 std::vector<std::unique_ptr<sdbusplus::bus::match_t>>
698     setupPropertiesChangedMatches(
699         sdbusplus::asio::connection& bus, std::span<const char* const> types,
700         const std::function<void(sdbusplus::message_t&)>& handler)
701 {
702     std::vector<std::unique_ptr<sdbusplus::bus::match_t>> matches;
703     for (const char* type : types)
704     {
705         auto match = std::make_unique<sdbusplus::bus::match_t>(
706             static_cast<sdbusplus::bus_t&>(bus),
707             "type='signal',member='PropertiesChanged',path_namespace='" +
708                 std::string(inventoryPath) + "',arg0namespace='" + type + "'",
709             handler);
710         matches.emplace_back(std::move(match));
711     }
712     return matches;
713 }
714