Implement cable monitor
Cable monitor consumes a cable configuration file which mentions which
cables are expected to be connected. This configuration file would be
staged by an external service running in the DC. Cable monitor also
consumes the Cable inventory from EM and compares that against the
expected cables from configuration file. Based on this, it generates
cable connected/disconnected events. For more information, refer to
design https://gerrit.openbmc.org/c/openbmc/docs/+/74799.
Related PRs:
https://gerrit.openbmc.org/c/openbmc/phosphor-dbus-interfaces/+/74397
Tested:
Testing has been performed with simulated configuration.
```
>> busctl tree xyz.openbmc_project.EntityManager
`- /xyz
`- /xyz/openbmc_project
|- /xyz/openbmc_project/EntityManager
`- /xyz/openbmc_project/inventory
`- /xyz/openbmc_project/inventory/system
`- /xyz/openbmc_project/inventory/system/cable
|- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable
| |- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable/TrayDetector1
| `- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable/TrayDetector2
`- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable_2
|- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable_2/TrayDetector3
`- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable_2/TrayDetector4
>> cat /var/lib/cablemonitor.d/cable-config.json
{
"ConnectedCables": ["Yosemite 4 Tray Cable", "Yosemite 4 Tray Cable 2"]
}
>> busctl tree xyz.openbmc_project.EntityManager
`- /xyz
`- /xyz/openbmc_project
|- /xyz/openbmc_project/EntityManager
`- /xyz/openbmc_project/inventory
`- /xyz/openbmc_project/inventory/system
`- /xyz/openbmc_project/inventory/system/cable
`- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable
|- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable/TrayDetector1
`- /xyz/openbmc_project/inventory/system/cable/Yosemite_4_Tray_Cable/TrayDetector2
>> 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/3",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3/attachment",
"Created": "2024-05-10T08:53:12.409+00:00",
"EntryType": "Event",
"Id": "3",
"Message": "xyz.openbmc_project.State.Cable.CableDisconnected",
"Modified": "2024-05-10T08:53:12.409+00:00",
"Name": "System Event Log Entry",
"Resolved": false,
"Severity": "Warning"
}
],
"Members@odata.count": 2,
"Name": "System Event Log Entries"
}
>> 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/3",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3/attachment",
"Created": "2024-05-10T08:53:12.409+00:00",
"EntryType": "Event",
"Id": "3",
"Message": "xyz.openbmc_project.State.Cable.CableDisconnected",
"Modified": "2024-05-10T08:54:13.691+00:00",
"Name": "System Event Log Entry",
"Resolved": true,
"Severity": "Warning"
},
{
"@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/5",
"@odata.type": "#LogEntry.v1_9_0.LogEntry",
"AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/5/attachment",
"Created": "2024-05-10T08:54:13.743+00:00",
"EntryType": "Event",
"Id": "5",
"Message": "xyz.openbmc_project.State.Cable.CableConnected",
"Modified": "2024-05-10T08:54:13.743+00:00",
"Name": "System Event Log Entry",
"Resolved": false,
"Severity": "OK"
}
],
"Members@odata.count": 4,
"Name": "System Event Log Entries"
}
```
Change-Id: Ic0552962406a95cc46d4dd1d83d72e68e0bd28b4
Signed-off-by: Jagpal Singh Gill <paligill@gmail.com>
diff --git a/src/cable-monitor/CableMonitor.cpp b/src/cable-monitor/CableMonitor.cpp
new file mode 100644
index 0000000..f57a722
--- /dev/null
+++ b/src/cable-monitor/CableMonitor.cpp
@@ -0,0 +1,193 @@
+#include "CableMonitor.hpp"
+
+#include "CableConfig.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <sdbusplus/async.hpp>
+#include <sdbusplus/async/stdexec/__detail/__then.hpp>
+#include <sdbusplus/message/native_types.hpp>
+#include <sdbusplus/server/manager.hpp>
+
+#include <chrono>
+#include <cstring>
+#include <filesystem>
+#include <functional>
+#include <string>
+
+PHOSPHOR_LOG2_USING;
+
+namespace cable
+{
+
+Monitor::Monitor(sdbusplus::async::context& ctx) :
+ ctx(ctx), cableEvents(ctx),
+ entityManager(ctx, {CableInventoryIntf::interface},
+ std::bind_front(&Monitor::inventoryAddedHandler, this),
+ std::bind_front(&Monitor::inventoryRemovedHandler, this)),
+ notifyWatch(ctx, Config::configFileDir,
+ std::bind_front(&Monitor::configUpdateHandler, this))
+{
+ ctx.spawn(start());
+}
+
+auto Monitor::inventoryAddedHandler(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& /*unused*/) -> void
+{
+ debug("Received cable added for {NAME}", "NAME", objectPath);
+ ctx.spawn(processCableAddedAsync(objectPath));
+}
+
+auto Monitor::inventoryRemovedHandler(
+ const sdbusplus::message::object_path& objectPath,
+ const std::string& /*unused*/) -> void
+{
+ debug("Received cable removed for {NAME}", "NAME", objectPath);
+ ctx.spawn(processCableRemovedAsync(objectPath));
+}
+
+auto Monitor::configUpdateHandler(std::string configFileName)
+ -> sdbusplus::async::task<>
+{
+ if (strcmp(Config::configFileName, configFileName.c_str()) != 0)
+ {
+ error("Update config file name {NAME} is not expected", "NAME",
+ configFileName);
+ co_return;
+ }
+ auto configFilePath =
+ std::string(Config::configFileDir) + "/" + configFileName;
+ if (!std::filesystem::exists(configFilePath))
+ {
+ error("Config file {NAME} does not exist", "NAME", configFilePath);
+ co_return;
+ }
+ expectedCables = co_await Config::processConfig(configFilePath);
+ if (expectedCables.empty())
+ {
+ error("No expected cables found in config file {NAME}", "NAME",
+ configFileName);
+ co_return;
+ }
+ co_await entityManager.handleInventoryGet();
+ ctx.spawn(sdbusplus::async::sleep_for(ctx, std::chrono::seconds(5)) |
+ stdexec::then([&]() { reconcileCableData(); }));
+}
+
+auto Monitor::start() -> sdbusplus::async::task<>
+{
+ info("Start cable monitor");
+
+ // Start async handler for cable config update
+ ctx.spawn(notifyWatch.readNotifyAsync());
+
+ // Process the cable config if it already exists
+ auto configFilePath =
+ std::string(Config::configFileDir) + "/" + Config::configFileName;
+ if (std::filesystem::exists(configFilePath))
+ {
+ co_await configUpdateHandler(Config::configFileName);
+ }
+
+ co_return;
+}
+
+auto Monitor::processCableAddedAsync(sdbusplus::message::object_path objectPath)
+ -> sdbusplus::async::task<>
+{
+ auto cableName = objectPath.filename();
+
+ debug("Received cable added for {NAME}", "NAME", cableName);
+
+ if (connectedCables.contains(cableName))
+ {
+ debug("Cable {NAME} is already connected, so skip it", "NAME",
+ cableName);
+ co_return;
+ }
+ else if (expectedCables.empty())
+ {
+ debug("No expected cables yet, so skip cable add for {NAME}", "NAME",
+ cableName);
+ co_return;
+ }
+ else if (!expectedCables.contains(cableName))
+ {
+ debug(
+ "Cable {NAME} is not in expected cables, skip connected event generation",
+ "NAME", cableName);
+ co_return;
+ }
+
+ connectedCables.insert(cableName);
+ co_await cableEvents.generateCableEvent(Events::Type::connected, cableName);
+ debug("New cable {NAME} added", "NAME", cableName);
+
+ co_return;
+}
+
+auto Monitor::processCableRemovedAsync(
+ sdbusplus::message::object_path objectPath) -> sdbusplus::async::task<>
+{
+ auto cableName = objectPath.filename();
+
+ debug("Received cable removed for {NAME}", "NAME", cableName);
+
+ if (expectedCables.empty())
+ {
+ debug("No expected cables yet, so skip cable add for {NAME}", "NAME",
+ cableName);
+ co_return;
+ }
+ else if (!expectedCables.contains(cableName))
+ {
+ debug(
+ "Cable {NAME} is not in expected cables, so skip disconnected event generation",
+ "NAME", cableName);
+ co_return;
+ }
+ else if (!connectedCables.contains(cableName))
+ {
+ debug(
+ "Cable {NAME} is not connected, so skip disconnected event generation",
+ "NAME", cableName);
+ co_return;
+ }
+
+ connectedCables.erase(cableName);
+ co_await cableEvents.generateCableEvent(Events::Type::disconnected,
+ cableName);
+ debug("Removed cable {NAME}", "NAME", cableName);
+
+ co_return;
+}
+
+auto Monitor::reconcileCableData() -> void
+{
+ for (const auto& cableName : expectedCables)
+ {
+ if (connectedCables.contains(cableName))
+ {
+ continue;
+ }
+ ctx.spawn(cableEvents.generateCableEvent(Events::Type::disconnected,
+ cableName));
+ }
+}
+
+} // namespace cable
+
+int main()
+{
+ constexpr auto path = "/xyz/openbmc_project/cable_monitor";
+ constexpr auto serviceName = "xyz.openbmc_project.cablemonitor";
+ sdbusplus::async::context ctx;
+ sdbusplus::server::manager_t manager{ctx, path};
+
+ info("Creating cable monitor");
+ cable::Monitor cableMonitor{ctx};
+ ctx.request_name(serviceName);
+
+ ctx.run();
+ return 0;
+}