Implement Liquid Leak Detector
Implement liquid leak detector based on design
https://gerrit.openbmc.org/c/openbmc/docs/+/73152.
Related PRs:
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/73151
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/73707
https://gerrit.openbmc.org/c/openbmc/sdbusplus/+/75461
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/75999
Tested:
```
>> busctl tree xyz.openbmc_project.LeakDetector
└─ /xyz
└─ /xyz/openbmc_project
└─ /xyz/openbmc_project/state
└─ /xyz/openbmc_project/state/leak
└─ /xyz/openbmc_project/state/leak/detector
├─ /xyz/openbmc_project/state/leak/detector/TrayDetector1
└─ /xyz/openbmc_project/state/leak/detector/TrayDetector2
>> echo pull-up > /sys/devices/platform/gpio-sim.0/gpiochip2/sim_gpio0/pull
>> curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Systems/system/LogServices/EventLog/Entries
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries",
"@odata.type": "#LogEntryCollection.LogEntryCollection",
"Description": "Collection of System Event Log Entries",
"Members": [
...
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2/attachment",
"Created": "2024-05-10T06:40:30.423+00:00",
"EntryType": "Event",
"Id": "2",
"Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedWarning",
"Modified": "2024-05-10T06:40:30.423+00:00",
"Name": "System Event Log Entry",
"Resolved": false,
"Severity": "Warning"
}
],
"Members@odata.count": 2,
"Name": "System Event Log Entries"
}
>> echo pull-down > /sys/devices/platform/gpio-sim.0/gpiochip2/sim_gpio0/pull
>> curl -k -H "X-Auth-Token: $token" -X GET https://${bmc}/redfish/v1/Systems/system/LogServices/EventLog/Entries
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries",
"@odata.type": "#LogEntryCollection.LogEntryCollection",
"Description": "Collection of System Event Log Entries",
"Members": [
...
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2/attachment",
"Created": "2024-05-10T06:40:30.423+00:00",
"EntryType": "Event",
"Id": "2",
"Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedWarning",
"Modified": "2024-05-10T06:42:23.989+00:00",
"Name": "System Event Log Entry",
"Resolved": true,
"Severity": "Warning"
},
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3/attachment",
"Created": "2024-05-10T06:42:24.024+00:00",
"EntryType": "Event",
"Id": "3",
"Message": "xyz.openbmc_project.State.Leak.Detector.LeakDetectedNormal",
"Modified": "2024-05-10T06:42:24.024+00:00",
"Name": "System Event Log Entry",
"Resolved": false,
"Severity": "OK"
}
],
"Members@odata.count": 3,
"Name": "System Event Log Entries"
}
```
Change-Id: Id6981deb314ca6852c5e31b932b28e601c2f3976
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/src/leakdetector/LeakDetectionManager.cpp b/src/leakdetector/LeakDetectionManager.cpp
new file mode 100644
index 0000000..5348f5f
--- /dev/null
+++ b/src/leakdetector/LeakDetectionManager.cpp
@@ -0,0 +1,158 @@
+#include "LeakDetectionManager.hpp"
+
+#include "LeakGPIODetector.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <sdbusplus/server/manager.hpp>
+
+#include <exception>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <string>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+DetectionManager::DetectionManager(sdbusplus::async::context& ctx) :
+ ctx(ctx), leakEvents(ctx),
+ entityManager(
+ ctx, {GPIODetectorConfigIntf::interface},
+ std::bind_front(&DetectionManager::processInventoryAdded, this),
+ std::bind_front(&DetectionManager::processInventoryRemoved, this))
+{
+ ctx.spawn(entityManager.handleInventoryGet());
+}
+
+auto DetectionManager::processInventoryAdded(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& /*unused*/) -> void
+{
+ ctx.spawn(processConfigAddedAsync(objectPath));
+}
+
+auto DetectionManager::processInventoryRemoved(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& /*unused*/) -> void
+{
+ if (!detectors.contains(objectPath.str))
+ {
+ return;
+ }
+ debug("Removed detector {DETECTOR}", "DETECTOR", objectPath);
+ detectors.erase(objectPath.str);
+}
+
+auto DetectionManager::processConfigAddedAsync(
+ sdbusplus::message::object_path objectPath) -> sdbusplus::async::task<>
+{
+ auto res = co_await getDetectorConfig(objectPath);
+ if (!res)
+ {
+ co_return;
+ }
+ auto config = res.value();
+
+ if (detectors.contains(objectPath.str))
+ {
+ warning("Detector {DETECTOR} already exist", "DETECTOR", config.name);
+ co_return;
+ }
+
+ try
+ {
+ detectors[objectPath.str] =
+ std::make_unique<GPIODetector>(ctx, leakEvents, config);
+ }
+ catch (std::exception& e)
+ {
+ error("Failed to create detector {DETECTOR}: {ERROR}", "DETECTOR",
+ config.name, "ERROR", e.what());
+ }
+
+ co_return;
+}
+
+auto DetectionManager::getDetectorConfig(
+ sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<std::optional<config::DetectorConfig>>
+{
+ config::DetectorConfig config = {};
+
+ auto properties =
+ co_await GPIODetectorConfigIntf(ctx)
+ .service(entity_manager::EntityManagerInterface::serviceName)
+ .path(objectPath.str)
+ .properties();
+
+ config.name = properties.name;
+
+ for (const auto& [key, value] : config::validDetectorTypes)
+ {
+ if (properties.type == key)
+ {
+ config.type = value;
+ break;
+ }
+ }
+
+ config.pinName = properties.pin_name;
+
+ for (const auto& [key, value] : config::validPinPolarity)
+ {
+ if (properties.polarity == key)
+ {
+ config.polarity = value;
+ break;
+ }
+ }
+ if (config.polarity == config::PinPolarity::unknown)
+ {
+ error("Invalid polarity {POLARITY} for {NAME}", "POLARITY",
+ properties.polarity, "NAME", config.name);
+ co_return std::nullopt;
+ }
+
+ for (const auto& [key, value] : config::validDetectorLevel)
+ {
+ if (properties.level == key)
+ {
+ config.level = value;
+ break;
+ }
+ }
+ if (config.level == config::DetectorLevel::unknown)
+ {
+ error("Invalid level {LEVEL} for {NAME}", "LEVEL", properties.level,
+ "NAME", config.name);
+ co_return std::nullopt;
+ }
+
+ debug("Detector config: {NAME} {PIN_NAME} {POLARITY} {LEVEL}", "NAME",
+ config.name, "PIN_NAME", config.pinName, "POLARITY", config.polarity,
+ "LEVEL", config.level);
+
+ co_return config;
+}
+
+} // namespace leak
+
+int main()
+{
+ constexpr auto path = leak::DetectorIntf::namespace_path::value;
+ constexpr auto serviceName = "xyz.openbmc_project.leakdetector";
+ sdbusplus::async::context ctx;
+ sdbusplus::server::manager_t manager{ctx, path};
+
+ info("Creating leak detection manager at {PATH}", "PATH", path);
+ leak::DetectionManager leakDetectionManager{ctx};
+
+ ctx.request_name(serviceName);
+
+ ctx.run();
+ return 0;
+}
diff --git a/src/leakdetector/LeakDetectionManager.hpp b/src/leakdetector/LeakDetectionManager.hpp
new file mode 100644
index 0000000..34111b9
--- /dev/null
+++ b/src/leakdetector/LeakDetectionManager.hpp
@@ -0,0 +1,57 @@
+#include "EntityManagerInterface.hpp"
+#include "LeakEvents.hpp"
+#include "LeakGPIODetector.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/Configuration/GPIOLeakDetector/client.hpp>
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+
+namespace leak
+{
+
+class DetectionManager;
+
+using GPIODetectorConfigIntf =
+ sdbusplus::client::xyz::openbmc_project::configuration::GPIOLeakDetector<>;
+
+class DetectionManager
+{
+ public:
+ DetectionManager() = delete;
+
+ explicit DetectionManager(sdbusplus::async::context& ctx);
+
+ private:
+ using detector_map_t =
+ std::unordered_map<std::string, std::unique_ptr<GPIODetector>>;
+
+ /** @brief Process new interfaces added to inventory */
+ auto processInventoryAdded(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName) -> void;
+
+ /** @brief Process interfaces removed from inventory */
+ auto processInventoryRemoved(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& interfaceName) -> void;
+
+ /** @brief Process the config add asynchronously */
+ auto processConfigAddedAsync(sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<>;
+
+ /** @brief Get the detector configuration from the Entity Manager */
+ auto getDetectorConfig(sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<std::optional<config::DetectorConfig>>;
+
+ sdbusplus::async::context& ctx;
+ Events leakEvents;
+ entity_manager::EntityManagerInterface entityManager;
+ detector_map_t detectors;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/LeakEvents.cpp b/src/leakdetector/LeakEvents.cpp
new file mode 100644
index 0000000..e3755a7
--- /dev/null
+++ b/src/leakdetector/LeakEvents.cpp
@@ -0,0 +1,64 @@
+#include "LeakEvents.hpp"
+
+#include "LeakGPIODetector.hpp"
+
+#include <phosphor-logging/commit.hpp>
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/event.hpp>
+
+#include <tuple>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+auto Events::generateLeakEvent(sdbusplus::message::object_path detectorPath,
+ DetectorStateIntf::DetectorState state,
+ config::DetectorLevel level)
+ -> sdbusplus::async::task<>
+{
+ auto eventName = std::make_tuple(detectorPath.str, level);
+
+ // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.Branch)
+ if (state == DetectorStateIntf::DetectorState::Normal)
+ {
+ auto pendingEvent = pendingEvents.find(eventName);
+ if (pendingEvent != pendingEvents.end())
+ {
+ co_await lg2::resolve(ctx, pendingEvent->second);
+
+ using DetectorNormal = sdbusplus::event::xyz::openbmc_project::
+ state::leak::Detector::LeakDetectedNormal;
+ co_await lg2::commit(ctx,
+ DetectorNormal("DETECTOR_NAME", detectorPath));
+
+ pendingEvents.erase(eventName);
+ }
+ co_return;
+ }
+
+ namespace error_intf =
+ sdbusplus::error::xyz::openbmc_project::state::leak::Detector;
+ sdbusplus::message::object_path eventPath{};
+
+ if (level == config::DetectorLevel::critical)
+ {
+ eventPath = co_await lg2::commit(
+ ctx,
+ error_intf::LeakDetectedCritical("DETECTOR_NAME", detectorPath));
+ error("Critical leak detected for {PATH}", "PATH", detectorPath);
+ }
+ else
+ {
+ eventPath = co_await lg2::commit(
+ ctx,
+ error_intf::LeakDetectedWarning("DETECTOR_NAME", detectorPath));
+ warning("Warning leak detected for {PATH}", "PATH", detectorPath);
+ }
+ pendingEvents[eventName] = eventPath;
+}
+
+} // namespace leak
diff --git a/src/leakdetector/LeakEvents.hpp b/src/leakdetector/LeakEvents.hpp
new file mode 100644
index 0000000..ef63205
--- /dev/null
+++ b/src/leakdetector/LeakEvents.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/client.hpp>
+
+#include <map>
+#include <string>
+#include <tuple>
+
+namespace leak
+{
+
+namespace config
+{
+enum class DetectorLevel;
+}
+
+using DetectorStateIntf =
+ sdbusplus::client::xyz::openbmc_project::state::leak::Detector<>;
+
+class Events
+{
+ public:
+ Events() = delete;
+
+ explicit Events(sdbusplus::async::context& ctx) : ctx(ctx) {}
+
+ auto generateLeakEvent(sdbusplus::message::object_path detectorPath,
+ DetectorStateIntf::DetectorState state,
+ config::DetectorLevel level)
+ -> sdbusplus::async::task<>;
+
+ private:
+ /** @brief Map type for event name to log event object path */
+ using event_key_t = std::tuple<std::string, config::DetectorLevel>;
+ using event_map_t = std::map<event_key_t, sdbusplus::message::object_path>;
+
+ sdbusplus::async::context& ctx;
+ event_map_t pendingEvents;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/LeakGPIODetector.cpp b/src/leakdetector/LeakGPIODetector.cpp
new file mode 100644
index 0000000..f21d1b5
--- /dev/null
+++ b/src/leakdetector/LeakGPIODetector.cpp
@@ -0,0 +1,89 @@
+#include "LeakGPIODetector.hpp"
+
+#include "LeakEvents.hpp"
+#include "SystemdInterface.hpp"
+
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/message/native_types.hpp>
+
+#include <array>
+#include <functional>
+#include <string>
+#include <string_view>
+#include <utility>
+
+namespace leak
+{
+
+namespace config
+{
+
+/** @brief Leak level to systemd target service map */
+static constexpr std::array<std::pair<config::DetectorLevel, std::string_view>,
+ 2>
+ leakActionTargets = {{{config::DetectorLevel::warning,
+ "xyz.openbmc_project.leakdetector.warning@"},
+ {config::DetectorLevel::critical,
+ "xyz.openbmc_project.leakdetector.critical@"}}};
+
+} // namespace config
+
+static auto getObjectPath(const std::string& detectorName)
+ -> sdbusplus::message::object_path
+{
+ return (
+ sdbusplus::message::object_path(DetectorIntf::namespace_path::value) /
+ DetectorIntf::namespace_path::detector / detectorName);
+}
+
+GPIODetector::GPIODetector(sdbusplus::async::context& ctx, Events& leakEvents,
+ const config::DetectorConfig& config) :
+ DetectorIntf(ctx, getObjectPath(config.name).str.c_str()), ctx(ctx),
+ leakEvents(leakEvents), config(config),
+ gpioInterface(ctx, config.name, config.pinName,
+ (config.polarity == config::PinPolarity::activeLow),
+ std::bind_front(&GPIODetector::updateGPIOStateAsync, this))
+{
+ pretty_name<false>(config.name);
+ type<false>(config.type);
+
+ ctx.spawn(gpioInterface.start());
+
+ debug("Created leak detector {NAME}", "NAME", config.name);
+}
+
+auto GPIODetector::updateGPIOStateAsync(bool gpioState)
+ -> sdbusplus::async::task<>
+{
+ auto newState = gpioState ? DetectorIntf::DetectorState::Abnormal
+ : DetectorIntf::DetectorState::Normal;
+
+ debug("Updating detector {DETECTOR} state to {STATE}", "DETECTOR",
+ config.name, "STATE", newState);
+
+ if (newState != state_)
+ {
+ state(newState);
+
+ co_await leakEvents.generateLeakEvent(getObjectPath(config.name),
+ state_, config.level);
+ if (state_ != DetectorIntf::DetectorState::Normal)
+ {
+ for (const auto& [level, serviceSuffix] : config::leakActionTargets)
+ {
+ if (config.level == level)
+ {
+ auto target = std::string(serviceSuffix) + config.name +
+ ".service";
+ debug("Starting systemd target {TARGET}", "TARGET", target);
+ co_await systemd::SystemdInterface::startUnit(ctx, target);
+ break;
+ }
+ }
+ }
+ }
+
+ co_return;
+}
+
+} // namespace leak
diff --git a/src/leakdetector/LeakGPIODetector.hpp b/src/leakdetector/LeakGPIODetector.hpp
new file mode 100644
index 0000000..074e758
--- /dev/null
+++ b/src/leakdetector/LeakGPIODetector.hpp
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "GPIOInterface.hpp"
+#include "LeakEvents.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/server.hpp>
+#include <xyz/openbmc_project/Association/Definitions/aserver.hpp>
+#include <xyz/openbmc_project/Configuration/GPIOLeakDetector/client.hpp>
+#include <xyz/openbmc_project/State/Leak/Detector/aserver.hpp>
+
+#include <array>
+#include <string>
+#include <string_view>
+#include <utility>
+
+PHOSPHOR_LOG2_USING;
+
+namespace leak
+{
+
+class GPIODetector;
+
+using DetectorConfigIntf =
+ sdbusplus::client::xyz::openbmc_project::configuration::GPIOLeakDetector<>;
+
+using DetectorIntf = sdbusplus::async::server_t<
+ GPIODetector,
+ sdbusplus::aserver::xyz::openbmc_project::association::Definitions,
+ sdbusplus::aserver::xyz::openbmc_project::state::leak::Detector>;
+
+namespace config
+{
+
+/** @brief Detector type to enum map */
+static constexpr std::array<
+ std::pair<std::string_view, DetectorIntf::DetectorType>, 2>
+ validDetectorTypes = {
+ {{"LeakSensingCable", DetectorIntf::DetectorType::LeakSensingCable},
+ {"Unknown", DetectorIntf::DetectorType::Unknown}}};
+
+/** @brief GPIO polarity */
+enum class PinPolarity
+{
+ activeLow,
+ activeHigh,
+ unknown
+};
+
+/** @brief Polarity name to enum map */
+static constexpr std::array<std::pair<std::string_view, PinPolarity>, 2>
+ validPinPolarity = {
+ {{"Low", PinPolarity::activeLow}, {"High", PinPolarity::activeHigh}}};
+
+/** @brief Detector level */
+enum class DetectorLevel
+{
+ critical,
+ warning,
+ unknown
+};
+
+/** @brief Leak detector level name to enum map */
+static constexpr std::array<std::pair<std::string_view, DetectorLevel>, 2>
+ validDetectorLevel = {{{"Warning", DetectorLevel::warning},
+ {"Critical", DetectorLevel::critical}}};
+
+/** @brief Leak detector configuration */
+struct DetectorConfig
+{
+ std::string name = Defaults::name;
+ DetectorIntf::DetectorType type = DetectorIntf::DetectorType::Unknown;
+ std::string pinName = Defaults::pinName;
+ PinPolarity polarity = PinPolarity::unknown;
+ DetectorLevel level = DetectorLevel::unknown;
+
+ struct Defaults
+ {
+ static constexpr auto name = "unknown";
+ static constexpr auto pinName = "unknown";
+ };
+};
+
+}; // namespace config
+
+class GPIODetector : public DetectorIntf
+{
+ public:
+ explicit GPIODetector(sdbusplus::async::context& ctx, Events& leakEvents,
+ const config::DetectorConfig& config);
+
+ auto updateGPIOStateAsync(bool gpioState) -> sdbusplus::async::task<>;
+
+ private:
+ sdbusplus::async::context& ctx;
+ Events& leakEvents;
+ config::DetectorConfig config;
+ gpio::GPIOInterface gpioInterface;
+};
+
+} // namespace leak
diff --git a/src/leakdetector/meson.build b/src/leakdetector/meson.build
new file mode 100644
index 0000000..aa953e4
--- /dev/null
+++ b/src/leakdetector/meson.build
@@ -0,0 +1,21 @@
+src_inc = include_directories('..')
+phosphor_dbus_interfaces_dep = dependency('phosphor-dbus-interfaces')
+
+executable(
+ 'leakdetector',
+ 'LeakDetectionManager.cpp',
+ 'LeakGPIODetector.cpp',
+ 'LeakEvents.cpp',
+ systemdinterface_src,
+ dependencies: [
+ default_deps,
+ utils_dep,
+ phosphor_dbus_interfaces_dep,
+ gpiodcxx,
+ gpiointerface_dep,
+ entitymanagerinterface_dep,
+ ],
+ include_directories: src_inc,
+ install: true,
+ install_dir: get_option('libexecdir') / 'dbus-sensors',
+)