HardwareState: HWMon data reader
Ported HWMon driver from
cla-sysrepo@19163e50ab062a8b5b75f754b30c22fdbd62b03a.
In cla-sysrepo, these drivers provided a property-based API, i.e., you
asked for a specific property (i.e., a filename) and the contents of the
file was returned.
In velia, we do not need this one-by-one property access. We provide all
attributes with a single call.
Change-Id: Ife783f21d4552e3b8bc69436d2f8fa53eb6745b7
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a29a1c8..f2e5a67 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -93,11 +93,14 @@
add_library(velia-ietf-hardware STATIC
src/ietf-hardware/sysfs/EMMC.cpp
src/ietf-hardware/sysfs/EMMC.h
+ src/ietf-hardware/sysfs/HWMon.cpp
+ src/ietf-hardware/sysfs/HWMon.h
)
target_link_libraries(velia-ietf-hardware
PUBLIC
velia-utils
${STD_FILESYSTEM_LIBRARY}
+ Boost::boost
)
target_include_directories(velia-ietf-hardware PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/velia-hardware)
@@ -131,6 +134,17 @@
SDBusCpp::sdbus-c++
)
+ add_library(FsTestUtils STATIC
+ tests/fs-helpers/FileInjector.cpp
+ tests/fs-helpers/FileInjector.h
+ tests/fs-helpers/utils.cpp
+ tests/fs-helpers/utils.h
+ )
+ target_link_libraries(FsTestUtils
+ PUBLIC
+ ${STD_FILESYSTEM_LIBRARY}
+ )
+
function(velia_test name)
add_executable(test-${name} ${CMAKE_SOURCE_DIR}/tests/${name}.cpp)
target_link_libraries(test-${name} DoctestIntegration ${ARGN})
@@ -151,7 +165,8 @@
velia_test(health_input-systemd velia-health DbusTesting)
velia_test(health_output-led velia-health)
- velia_test(hardware_emmc velia-ietf-hardware)
+ velia_test(hardware_emmc velia-ietf-hardware FsTestUtils)
+ velia_test(hardware_hwmon velia-ietf-hardware FsTestUtils)
endif()
if(WITH_DOCS)
diff --git a/src/ietf-hardware/sysfs/HWMon.cpp b/src/ietf-hardware/sysfs/HWMon.cpp
new file mode 100644
index 0000000..a34607f
--- /dev/null
+++ b/src/ietf-hardware/sysfs/HWMon.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2017-2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Miroslav Mareš <mmares@cesnet.cz>
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+*/
+#include <algorithm>
+#include <boost/algorithm/string/predicate.hpp>
+#include <filesystem>
+#include "HWMon.h"
+#include "utils/io.h"
+#include "utils/log.h"
+
+namespace velia::ietf_hardware::sysfs {
+
+using namespace std::literals;
+
+/** @class HWMon
+
+@short Implements access to sensor chips data in specific hwmon directory.
+
+This class provides property-like access to various sensoric data from kernel hwmon subsystem.
+
+Docs: https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface
+Kernel: https://github.com/torvalds/linux/tree/master/drivers/hwmon
+*/
+
+
+namespace {
+
+/** @brief Only files from hwmon directory that end with these suffixes are considered */
+static const std::vector<std::string> ACCEPTED_FILE_ENDINGS {
+ "_input",
+ "_crit",
+ "_min",
+ "_max",
+ "_average",
+ "_highest",
+ "_lowest",
+};
+}
+
+/**
+ * @short Constructs a HWMon driver for hwmon entries
+ * @param root A path to the hwmon using specific device directory from /sys/devices/ or /sys/bus/i2c, e.g.: /sys/devices/platform/soc/soc:internal-regs/f1011100.i2c/i2c-1/1-002e/hwmon or /sys/bus/i2c/devices/2-0025/hwmon
+ * */
+HWMon::HWMon(std::filesystem::path hwmonDir)
+ : m_log(spdlog::get("hardware"))
+{
+ // Find root directory (should be called hwmonX)
+ std::vector<std::filesystem::path> rootCandidates;
+ if (std::filesystem::exists(hwmonDir)) {
+ for (const auto& e : std::filesystem::directory_iterator(hwmonDir)) {
+ // only directories hwmon<int> with a file named 'name' (required by kernel docs) are valid
+ if (e.is_directory() && boost::algorithm::starts_with(std::string(e.path().filename()), "hwmon") && std::filesystem::exists(e.path() / "name"s)) {
+ rootCandidates.push_back(e.path());
+ m_log->trace("hwmon: Found a candidate: {}", std::string(e.path()));
+ }
+ }
+ }
+
+ if (rootCandidates.size() != 1)
+ throw std::invalid_argument("Invalid hwmon directory ('" + std::string(hwmonDir) + "')");
+ m_root = rootCandidates.front();
+
+ m_log->trace("HWMon() driver initialized for '{}'", std::string(m_root));
+
+ // Scan through files in root directory, discard directories, non-readable files and non-interesting (see accepted_endings) files
+ for (const auto& entry : std::filesystem::directory_iterator(m_root)) {
+ if (!std::filesystem::is_regular_file(entry.path())) {
+ continue;
+ }
+
+ if (std::any_of(ACCEPTED_FILE_ENDINGS.cbegin(), ACCEPTED_FILE_ENDINGS.cend(), [&entry](const auto& ending) { return boost::algorithm::ends_with(std::string(entry.path().filename()), ending); })) {
+ m_properties.push_back(entry.path().filename());
+ }
+ }
+}
+
+HWMon::~HWMon() = default;
+
+/** @brief Return attributes read by this hwmon.
+ *
+ * For the return value discussion @see HWMon::readFile
+ */
+HWMon::Attributes HWMon::attributes() const
+{
+ Attributes result;
+
+ for (const auto& propertyName : m_properties) {
+ // Read int64_t value because kernel seems to print numeric values as signed long ints (@see linux/drivers/hwmon/hwmon.c)
+ result.insert(std::make_pair(propertyName, velia::utils::readFileInt64(m_root / propertyName)));
+ }
+
+ return result;
+}
+}
diff --git a/src/ietf-hardware/sysfs/HWMon.h b/src/ietf-hardware/sysfs/HWMon.h
new file mode 100644
index 0000000..e3b3932
--- /dev/null
+++ b/src/ietf-hardware/sysfs/HWMon.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017-2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Miroslav Mareš <mmares@cesnet.cz>
+ * Written by Tomáš Pecka <tomas.pecka@fit.cvut.cz>
+ *
+*/
+
+#pragma once
+
+#include <filesystem>
+#include <map>
+#include <vector>
+#include "utils/log-fwd.h"
+
+namespace velia::ietf_hardware::sysfs {
+
+class HWMon {
+public:
+ using Attributes = std::map<std::string, int64_t>;
+
+ explicit HWMon(std::filesystem::path hwmonDir);
+ ~HWMon();
+
+ Attributes attributes() const;
+
+private:
+ velia::Log m_log;
+
+ /** @brief path to the real hwmon directory */
+ std::filesystem::path m_root;
+
+ /** @brief keys of entries that are exported via this hwmon. Filled by constructor. */
+ std::vector<std::string> m_properties;
+};
+}
diff --git a/src/utils/io.cpp b/src/utils/io.cpp
index 4a18668..86dbd7c 100644
--- a/src/utils/io.cpp
+++ b/src/utils/io.cpp
@@ -53,4 +53,17 @@
return bytes;
}
+/** @brief Reads a int64_t number from a file. */
+int64_t readFileInt64(const std::filesystem::path& path)
+{
+ std::ifstream ifs(openStream(path));
+ int64_t res;
+
+ if (ifs >> res) {
+ return res;
+ }
+
+ throw std::domain_error("Could not read int64_t value from '" + std::string(path) + "'.");
+}
+
}
diff --git a/src/utils/io.h b/src/utils/io.h
index a23069d..6d8123a 100644
--- a/src/utils/io.h
+++ b/src/utils/io.h
@@ -14,5 +14,6 @@
namespace velia::utils {
std::string readFileString(const std::filesystem::path& path);
+int64_t readFileInt64(const std::filesystem::path& path);
std::vector<uint32_t> readFileWords(const std::filesystem::path& path, int valuesCount);
}
diff --git a/tests/fs-helpers/FileInjector.cpp b/tests/fs-helpers/FileInjector.cpp
new file mode 100644
index 0000000..a3a80f2
--- /dev/null
+++ b/tests/fs-helpers/FileInjector.cpp
@@ -0,0 +1,26 @@
+#include <fstream>
+#include "FileInjector.h"
+
+/** @short Creates a file with specific permissions and content */
+FileInjector::FileInjector(const std::filesystem::path& path, const std::filesystem::perms permissions, const std::string& content)
+ : path(path)
+{
+ auto fileStream = std::ofstream(path, std::ios_base::out | std::ios_base::trunc);
+ if (!fileStream.is_open()) {
+ throw std::invalid_argument("FileInjector could not open file " + std::string(path) + " for writing");
+ }
+ fileStream << content;
+ std::filesystem::permissions(path, permissions);
+}
+
+/** @short Removes file associated with this FileInjector instance (if exists) */
+FileInjector::~FileInjector() noexcept(false)
+{
+ std::filesystem::remove(path);
+}
+
+/** @short Sets file permissions */
+void FileInjector::setPermissions(const std::filesystem::perms permissions)
+{
+ std::filesystem::permissions(path, permissions);
+}
diff --git a/tests/fs-helpers/FileInjector.h b/tests/fs-helpers/FileInjector.h
new file mode 100644
index 0000000..873d3b6
--- /dev/null
+++ b/tests/fs-helpers/FileInjector.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <filesystem>
+#include <string>
+
+/** @short Represents a temporary file whose lifetime is bound by lifetime of the FileInjector instance */
+class FileInjector {
+private:
+ const std::string path;
+
+public:
+ FileInjector(const std::filesystem::path& path, const std::filesystem::perms permissions, const std::string& content);
+ ~FileInjector() noexcept(false);
+ void setPermissions(const std::filesystem::perms permissions);
+};
diff --git a/tests/fs-helpers/utils.cpp b/tests/fs-helpers/utils.cpp
new file mode 100644
index 0000000..1fc018a
--- /dev/null
+++ b/tests/fs-helpers/utils.cpp
@@ -0,0 +1,9 @@
+#include "utils.h"
+
+/** @short Remove directory tree at 'rootDir' path (if exists) */
+void removeDirectoryTreeIfExists(const std::filesystem::path& rootDir)
+{
+ if (std::filesystem::exists(rootDir)) {
+ std::filesystem::remove_all(rootDir);
+ }
+}
diff --git a/tests/fs-helpers/utils.h b/tests/fs-helpers/utils.h
new file mode 100644
index 0000000..93196f2
--- /dev/null
+++ b/tests/fs-helpers/utils.h
@@ -0,0 +1,4 @@
+#pragma once
+#include <filesystem>
+
+void removeDirectoryTreeIfExists(const std::filesystem::path& rootDir);
diff --git a/tests/hardware_emmc.cpp b/tests/hardware_emmc.cpp
index 91b06c0..4707686 100644
--- a/tests/hardware_emmc.cpp
+++ b/tests/hardware_emmc.cpp
@@ -7,6 +7,7 @@
#include "trompeloeil_doctest.h"
#include <filesystem>
+#include "fs-helpers/utils.h"
#include "ietf-hardware/sysfs/EMMC.h"
#include "pretty_printers.h"
#include "test_log_setup.h"
@@ -14,17 +15,6 @@
using namespace std::literals;
-namespace {
-
-/** @short Remove directory tree at 'rootDir' path (if exists) */
-void removeDirectoryTreeIfExists(const std::string& rootDir)
-{
- if (std::filesystem::exists(rootDir)) {
- std::filesystem::remove_all(rootDir);
- }
-}
-}
-
TEST_CASE("EMMC driver")
{
TEST_INIT_LOGS;
diff --git a/tests/hardware_hwmon.cpp b/tests/hardware_hwmon.cpp
new file mode 100644
index 0000000..3f9fb63
--- /dev/null
+++ b/tests/hardware_hwmon.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017-2018 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Miroslav Mareš <mmares@cesnet.cz>
+ *
+*/
+
+#include "trompeloeil_doctest.h"
+#include <filesystem>
+#include <fstream>
+#include "fs-helpers/FileInjector.h"
+#include "fs-helpers/utils.h"
+#include "ietf-hardware/sysfs/HWMon.h"
+#include "pretty_printers.h"
+#include "test_log_setup.h"
+#include "tests/configure.cmake.h"
+
+using namespace std::literals;
+
+TEST_CASE("HWMon class")
+{
+ TEST_INIT_LOGS;
+
+ const auto fakeHwmonRoot = CMAKE_CURRENT_BINARY_DIR + "/tests/hwmon/"s;
+ removeDirectoryTreeIfExists(fakeHwmonRoot);
+ velia::ietf_hardware::sysfs::HWMon::Attributes expected;
+
+ SECTION("Test hwmon/device1")
+ {
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+ expected = {
+ {"temp1_crit", 105'000},
+ {"temp1_input", 66'600},
+ {"temp2_crit", 105'000},
+ {"temp2_input", 29'800},
+ {"temp10_crit", 666'777},
+ {"temp10_input", 66'600},
+ {"temp11_input", 111'222'333'444'555},
+ };
+
+ REQUIRE(hwmon.attributes() == expected);
+ }
+
+ SECTION("Test hwmon/device1 + one of the files unreadable")
+ {
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+ // Inject temporary file for "no read permission" test
+ auto injected_noread = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_write, "-42001");
+
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+ expected = {
+ {"temp1_crit", 105'000},
+ {"temp1_input", 66'600},
+ {"temp2_crit", 105'000},
+ {"temp2_input", 29'800},
+ {"temp3_input", -42'001},
+ {"temp10_crit", 666'777},
+ {"temp10_input", 66'600},
+ {"temp11_input", 111'222'333'444'555},
+ };
+
+ // no read permission now
+ REQUIRE_THROWS_AS(hwmon.attributes(), std::invalid_argument);
+
+ // read permission granted
+ injected_noread->setPermissions(std::filesystem::perms::owner_all);
+ REQUIRE(hwmon.attributes() == expected);
+ }
+
+ SECTION("Test hwmon/device1 + one of the files disappears after construction")
+ {
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+ // Inject temporary file for "file does not exist" test
+ auto injected_notexist = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "-42001");
+
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+
+ expected = {
+ {"temp1_crit", 105'000},
+ {"temp1_input", 66'600},
+ {"temp2_crit", 105'000},
+ {"temp2_input", 29'800},
+ {"temp3_input", -42'001},
+ {"temp10_crit", 666'777},
+ {"temp10_input", 66'600},
+ {"temp11_input", 111'222'333'444'555},
+ };
+
+ // file exists, should be OK
+ REQUIRE(hwmon.attributes() == expected);
+
+ // file deleted
+ injected_notexist.reset();
+ REQUIRE_THROWS_AS(hwmon.attributes(), std::invalid_argument);
+ }
+
+ SECTION("Test hwmon/device1 + invalid values")
+ {
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+ SECTION("Invalid content")
+ {
+ auto injected = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "cus bus");
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+ REQUIRE_THROWS_AS(hwmon.attributes(), std::domain_error);
+ }
+
+ SECTION("Invalid value range")
+ {
+ auto injected = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "-99999999999999999999999999999999");
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+ REQUIRE_THROWS_AS(hwmon.attributes(), std::domain_error);
+ }
+ }
+
+ SECTION("Test hwmon/device2")
+ {
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device2/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+ auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+ expected = {
+ {"temp1_crit", std::numeric_limits<int64_t>::max()},
+ {"temp1_input", -34'000},
+ {"temp1_max", 80'000},
+ {"temp2_crit", std::numeric_limits<int64_t>::min()}, // we can't write an integer literal for int64_t min value (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52661)
+ {"temp2_input", -34'000},
+ {"temp2_max", 80'000},
+ {"temp3_crit", 100'000},
+ {"temp3_input", 30'000},
+ {"temp3_max", 80'000},
+ {"temp4_crit", 100'000},
+ {"temp4_input", 26'000},
+ {"temp4_max", 80'000},
+ {"temp5_crit", 100'000},
+ {"temp5_input", 29'000},
+ {"temp5_max", 80'000},
+ };
+
+ REQUIRE(hwmon.attributes() == expected);
+ }
+
+ SECTION("Test wrong directory structure")
+ {
+ std::string sourceDir;
+ SECTION("No hwmonX directory")
+ {
+ sourceDir = "tests/sysfs/hwmon/device4/hwmon"s;
+ }
+ SECTION("Multiple hwmonX directories")
+ {
+ sourceDir = "tests/sysfs/hwmon/device3/hwmon"s;
+ }
+
+ std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/"s + sourceDir, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+ REQUIRE_THROWS_AS(velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot), std::invalid_argument);
+ }
+}
diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h
index 8fea584..eb1399c 100644
--- a/tests/pretty_printers.h
+++ b/tests/pretty_printers.h
@@ -12,7 +12,6 @@
#include <sstream>
#include <trompeloeil.hpp>
-
namespace doctest {
template <>
@@ -29,4 +28,18 @@
}
};
+template <>
+struct StringMaker<std::map<std::string, int64_t>> {
+ static String convert(const std::map<std::string, int64_t>& map)
+ {
+ std::ostringstream os;
+ os << "{" << std::endl;
+ for (const auto& [key, value] : map) {
+ os << " \"" << key << "\": " << value << "," << std::endl;
+ }
+ os << "}";
+ return os.str().c_str();
+ }
+};
+
}
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/name b/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
index 2ec2faf..5f10796 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
@@ -1 +1 @@
-satan temperatures
+some small or large numbers are to be read here
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
index 363ca95..2045006 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
@@ -1 +1 @@
-1000000000000000000005666
+9223372036854775807
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
index 0277850..7928ab8 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
@@ -1 +1 @@
-nazdar bazar carodej Merlin
+-9223372036854775808
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
index 782a512..acab4b6 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
@@ -1 +1 @@
--1234567891234567896666
+-34000