Redfish: Implement SNMP Trap
Implement SNMPTrap in EventDestination of Redfish. We can use
this Redfish interface to add/get/delete the SNMPTrap port and
destination address. When the error
log is generated, phosphor-snmp
will send SNMPTrap messages to our configured SNMPTrap destination.
The MIB is here:
[1] https://github.com/openbmc/phosphor-snmp/blob/master/mibs/NotificationMIB.txt
Refer:
[1] https://www.dmtf.org/sites/default/files/standards/documents/DSP0268_2019.3.pdf
SNMPTrap test: Tested ok on the Witherspoon machine.
Steps are as follows:
1. Use this Redfish interface to configure the port and
destination address:
curl -k -H "X-Auth-Token: $token" -X POST
https://${bmc}/redfish/v1/EventService/Subscriptions
-d '{"Destination": "snmp://192.168.31.89:162",
"SubscriptionType": "SNMPTrap", "Protocol": "SNMPv2c"}'
2. Run the SNMPTrap receiver tool in the destination
computer(192.168.31.89),I used iReasoning MIB Browser as the
SNMPTrap receiving tool.
3. Trigger error logs such as power supply AC Lost. We will see
the error log under /xyz/openbmc_project/logging.
4. The SNMPTrap receiver tool in the destination computer received
the SNMPTrap sent by OpenBMC.
Tested: Validator passes
1. Add snmp client:
curl -k -H "X-Auth-Token: $token" -X POST
https://${bmc}/redfish/v1/EventService/Subscriptions
-d '{"Destination": "snmp://192.168.31.89:162",
"SubscriptionType": "SNMPTrap", "Protocol": "SNMPv2c",
"Context": "testContext"}'
{
"@Message.ExtendedInfo": [
{
"@odata.type": "#Message.v1_0_0.Message",
"Message": "The resource has been created successfully",
"MessageArgs": [],
"MessageId": "Base.1.8.1.Created",
"MessageSeverity": "OK",
"Resolution": "None"
}
]
}
2. Get snmp trap client configurations:
curl -k -H "X-Auth-Token: $token" -X GET
https://${bmc}/redfish/v1/EventService/Subscriptions/snmp1
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp1",
"@odata.type": "#EventDestination.v1_7_0.EventDestination",
"Context": "testContext",
"Destination": "snmp://192.168.31.89:162",
"EventFormatType": "Event",
"Id": "snmp1",
"Name": "Event Destination snmp1",
"Protocol": "SNMPv2c",
"SubscriptionType": "SNMPTrap"
}
Reboot the BMC, and get the snmp trap client again:
curl -k -H "X-Auth-Token: $token" -X GET
https://${bmc}/redfish/v1/EventService/Subscriptions/snmp1
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp1",
"@odata.type": "#EventDestination.v1_7_0.EventDestination",
"Context": "testContext",
"Destination": "snmp://192.168.31.89:162",
"EventFormatType": "Event",
"Id": "snmp1",
"Name": "Event Destination snmp1",
"Protocol": "SNMPv2c",
"SubscriptionType": "SNMPTrap"
}
3. Delete snmp client:
curl -k -H "X-Auth-Token: $token" -X DELETE
https://${bmc}/redfish/v1/EventService/Subscriptions/snmp1
{
"@Message.ExtendedInfo": [
{
"@odata.type": "#Message.v1_1_1.Message",
"Message": "Successfully Completed Request",
"MessageArgs": [],
"MessageId": "Base.1.8.1.Success",
"MessageSeverity": "OK",
"Resolution": "None"
}
]
}
4. After we have added some SNMP clients using Redfish, we can see them
in Dbus:
busctl tree xyz.openbmc_project.Network.SNMP
`-/xyz
`-/xyz/openbmc_project
`-/xyz/openbmc_project/network
`-/xyz/openbmc_project/network/snmp
`-/xyz/openbmc_project/network/snmp/manager
|-/xyz/openbmc_project/network/snmp/manager/1
busctl introspect xyz.openbmc_project.Network.SNMP
/xyz/openbmc_project/network/snmp/manager/1
xyz.openbmc_project.Network.Client
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
.Address property s "192.168.31.89" emits-change writable
.Port property q 162 emits-change writable
5. Use "busctl call" add client
busctl call xyz.openbmc_project.Network.SNMP
/xyz/openbmc_project/network/snmp/manager
xyz.openbmc_project.Network.Client.Create
Client sq 192.168.31.90 162
s "/xyz/openbmc_project/network/snmp/manager/2"
We will see it use the redfish url:
curl -k -H "X-Auth-Token: $token" -X GET
https://${bmc}/redfish/v1/EventService/Subscriptions/snmp2
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp2",
"@odata.type": "#EventDestination.v1_7_0.EventDestination",
"Context": "",
"Destination": "snmp://192.168.31.90:162",
"EventFormatType": "Event",
"Id": "snmp2",
"Name": "Event Destination snmp2",
"Protocol": "SNMPv2c",
"SubscriptionType": "SNMPTrap"
}
6. Deleting snmp client using "busctl"
First, we use redfish to add some SNMP clients:
curl -k -H "X-Auth-Token: $token" -X POST
https://${bmc}/redfish/v1/EventService/Subscriptions
-d '{"Destination": "snmp://192.168.31.90:162",
"SubscriptionType": "SNMPTrap", "Protocol": "SNMPv2c",
"Context": "testContext0"}'
curl -k -H "X-Auth-Token: $token" -X POST
https://${bmc}/redfish/v1/EventService/Subscriptions
-d '{"Destination": "snmp://192.168.31.91:162",
"SubscriptionType": "SNMPTrap", "Protocol": "SNMPv2c",
"Context": "testContext1"}'
Then we can use redfish to get the subscriptions:
curl -k -H "X-Auth-Token: $token" -XGET
https://${bmc}/redfish/v1/EventService/Subscriptions
{
"@odata.id": "/redfish/v1/EventService/Subscriptions",
"@odata.type":"#EventDestinationCollection.EventDestinationCollection",
"Members": [
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp1"
},
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp2"
}
],
"Members@odata.count": 2,
"Name": "Event Destination Collections"
}
Now we use busctl to delete SNMP client 2:
busctl call xyz.openbmc_project.Network.SNMP
/xyz/openbmc_project/network/snmp/manager/2
xyz.openbmc_project.Object.Delete Delete
Then we won't see snmp2 in the subscriptions of redfish:
curl -k -H "X-Auth-Token: $token" -XGET
https://${bmc}/redfish/v1/EventService/Subscriptions
{
"@odata.id": "/redfish/v1/EventService/Subscriptions",
"@odata.type":"#EventDestinationCollection.EventDestinationCollection",
"Members": [
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/snmp1"
}
],
"Members@odata.count": 1,
"Name": "Event Destination Collections"
}
7. Test the generic event subscription to make sure it didn't impacted
Add Redfish subscription:
curl -k -H "X-Auth-Token: $token" -X POST
https://${bmc}/redfish/v1/EventService/Subscriptions
-d '{"Destination": "https://192.168.31.189:443",
"SubscriptionType": "RedfishEvent", "Protocol": "Redfish",
"Context": "testContext"}'
{
"@Message.ExtendedInfo": [
{
"@odata.type": "#Message.v1_1_1.Message",
"Message": "The resource has been created successfully.",
"MessageArgs": [],
"MessageId": "Base.1.13.0.Created",
"MessageSeverity": "OK",
"Resolution": "None."
}
]
Get Redfish subscription:
curl -k -H "X-Auth-Token: $token" -X GET
https://${bmc}/redfish/v1/EventService/Subscriptions/1358109191
{
"@odata.id": "/redfish/v1/EventService/Subscriptions/1358109191",
"@odata.type": "#EventDestination.v1_8_0.EventDestination",
"Context": "testContext",
"DeliveryRetryPolicy": "TerminateAfterRetries",
"Destination": "https://192.168.31.189:443",
"EventFormatType": "Event",
"HttpHeaders": [],
"Id": "1358109191",
"MessageIds": [],
"MetricReportDefinitions": [],
"Name": "Event Destination 1358109191",
"Protocol": "Redfish",
"RegistryPrefixes": [],
"ResourceTypes": [],
"SubscriptionType": "RedfishEvent"
}
Signed-off-by: Chicago Duan <duanzhijia01@inspur.com>
Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: Ie589b3934ee749c7e0add35e3ed1b0b7e817c557
diff --git a/redfish-core/include/snmp_trap_event_clients.hpp b/redfish-core/include/snmp_trap_event_clients.hpp
new file mode 100644
index 0000000..08ff81b
--- /dev/null
+++ b/redfish-core/include/snmp_trap_event_clients.hpp
@@ -0,0 +1,232 @@
+#pragma once
+
+#include "async_resp.hpp"
+#include "dbus_singleton.hpp"
+#include "dbus_utility.hpp"
+#include "error_messages.hpp"
+#include "event_service_manager.hpp"
+#include "http_request.hpp"
+#include "http_response.hpp"
+#include "logging.hpp"
+#include "utils/dbus_utils.hpp"
+
+#include <boost/system/error_code.hpp>
+#include <boost/url/format.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <memory>
+#include <string>
+#include <string_view>
+
+namespace redfish
+{
+
+inline void afterGetSnmpTrapClientdata(
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const boost::system::error_code& ec,
+ const dbus::utility::DBusPropertiesMap& propertiesList)
+{
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR << "D-Bus response error on GetSubTree " << ec;
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ std::string address;
+ uint16_t port = 0;
+
+ bool success = sdbusplus::unpackPropertiesNoThrow(
+ dbus_utils::UnpackErrorPrinter(), propertiesList, "Address", address,
+ "Port", port);
+
+ if (!success)
+ {
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ asyncResp->res.jsonValue["Destination"] =
+ boost::urls::format("snmp://{}:{}", address, port);
+}
+
+inline void
+ getSnmpTrapClientdata(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& id, const std::string& objectPath)
+{
+ asyncResp->res.jsonValue["@odata.type"] =
+ "#EventDestination.v1_8_0.EventDestination";
+ asyncResp->res.jsonValue["Protocol"] = "SNMPv2c";
+ asyncResp->res.jsonValue["@odata.id"] =
+ boost::urls::format("/redfish/v1/EventService/Subscriptions/{}", id);
+
+ asyncResp->res.jsonValue["Id"] = id;
+ asyncResp->res.jsonValue["Name"] = "Event Destination";
+
+ asyncResp->res.jsonValue["SubscriptionType"] = "SNMPTrap";
+ asyncResp->res.jsonValue["EventFormatType"] = "Event";
+
+ std::shared_ptr<Subscription> subValue =
+ EventServiceManager::getInstance().getSubscription(id);
+ if (subValue != nullptr)
+ {
+ asyncResp->res.jsonValue["Context"] = subValue->customText;
+ }
+ else
+ {
+ asyncResp->res.jsonValue["Context"] = "";
+ }
+
+ sdbusplus::asio::getAllProperties(
+ *crow::connections::systemBus, "xyz.openbmc_project.Network.SNMP",
+ objectPath, "xyz.openbmc_project.Network.Client",
+ [asyncResp](const boost::system::error_code& ec,
+ const dbus::utility::DBusPropertiesMap& properties) {
+ afterGetSnmpTrapClientdata(asyncResp, ec, properties);
+ });
+}
+
+inline void
+ getSnmpTrapClient(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& id)
+{
+ crow::connections::systemBus->async_method_call(
+ [asyncResp, id](const boost::system::error_code& ec,
+ dbus::utility::ManagedObjectType& resp) {
+ if (ec)
+ {
+ BMCWEB_LOG_ERROR << "D-Bus response error on GetManagedObjects "
+ << ec;
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ for (const auto& objpath : resp)
+ {
+ sdbusplus::message::object_path path(objpath.first);
+ const std::string snmpId = path.filename();
+ if (snmpId.empty())
+ {
+ BMCWEB_LOG_ERROR << "The SNMP client ID is wrong";
+ messages::internalError(asyncResp->res);
+ return;
+ }
+ const std::string subscriptionId = "snmp" + snmpId;
+ if (id != subscriptionId)
+ {
+ continue;
+ }
+
+ getSnmpTrapClientdata(asyncResp, id, objpath.first);
+ return;
+ }
+
+ messages::resourceNotFound(asyncResp->res, "Subscriptions", id);
+ EventServiceManager::getInstance().deleteSubscription(id);
+ },
+ "xyz.openbmc_project.Network.SNMP",
+ "/xyz/openbmc_project/network/snmp/manager",
+ "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
+}
+
+inline void
+ afterSnmpClientCreate(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const boost::system::error_code& ec,
+ const std::string& dbusSNMPid)
+{
+ if (ec)
+ {
+ if (ec.value() != EBADR)
+ {
+ // SNMP not installed
+ messages::propertyValueOutOfRange(asyncResp->res, "SNMPv2c",
+ "Protocol");
+ return;
+ }
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ sdbusplus::message::object_path path(dbusSNMPid);
+ const std::string snmpId = path.filename();
+ if (snmpId.empty())
+ {
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ std::string subscriptionId = "snmp" + snmpId;
+
+ boost::urls::url uri = boost::urls::format(
+ "/redfish/v1/EventService/Subscriptions/{}", subscriptionId);
+ asyncResp->res.addHeader("Location", uri.buffer());
+ messages::created(asyncResp->res);
+}
+
+inline void
+ addSnmpTrapClient(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& host, uint16_t snmpTrapPort)
+{
+ crow::connections::systemBus->async_method_call(
+ [asyncResp](const boost::system::error_code& ec,
+ const std::string& dbusSNMPid) {
+ afterSnmpClientCreate(asyncResp, ec, dbusSNMPid);
+ },
+ "xyz.openbmc_project.Network.SNMP",
+ "/xyz/openbmc_project/network/snmp/manager",
+ "xyz.openbmc_project.Network.Client.Create", "Client", host,
+ snmpTrapPort);
+}
+
+inline void
+ getSnmpSubscriptionList(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& snmpId,
+ nlohmann::json& memberArray)
+{
+ const std::string subscriptionId = "snmp" + snmpId;
+
+ nlohmann::json::object_t member;
+ member["@odata.id"] = boost::urls::format(
+ "/redfish/v1/EventService/Subscriptions/{}", subscriptionId);
+ memberArray.push_back(std::move(member));
+
+ asyncResp->res.jsonValue["Members@odata.count"] = memberArray.size();
+}
+
+inline void
+ deleteSnmpTrapClient(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const std::string& param)
+{
+ std::string_view snmpTrapId = param;
+
+ // Erase "snmp" in the request to find the corresponding
+ // dbus snmp client id. For example, the snmpid in the
+ // request is "snmp1", which will be "1" after being erased.
+ snmpTrapId.remove_prefix(4);
+
+ sdbusplus::message::object_path snmpPath =
+ sdbusplus::message::object_path(
+ "/xyz/openbmc_project/network/snmp/manager") /
+ std::string(snmpTrapId);
+
+ crow::connections::systemBus->async_method_call(
+ [asyncResp, param](const boost::system::error_code& ec) {
+ if (ec)
+ {
+ // The snmp trap id is incorrect
+ if (ec.value() == EBADR)
+ {
+ messages::resourceNotFound(asyncResp->res, "Subscription",
+ param);
+ return;
+ }
+ messages::internalError(asyncResp->res);
+ return;
+ }
+ messages::success(asyncResp->res);
+ },
+ "xyz.openbmc_project.Network.SNMP", static_cast<std::string>(snmpPath),
+ "xyz.openbmc_project.Object.Delete", "Delete");
+}
+
+} // namespace redfish
diff --git a/redfish-core/lib/event_service.hpp b/redfish-core/lib/event_service.hpp
index 28a7ac6..aef4d47 100644
--- a/redfish-core/lib/event_service.hpp
+++ b/redfish-core/lib/event_service.hpp
@@ -20,10 +20,17 @@
#include "logging.hpp"
#include "query.hpp"
#include "registries/privilege_registry.hpp"
+#include "snmp_trap_event_clients.hpp"
#include <boost/beast/http/fields.hpp>
+#include <boost/system/error_code.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+#include <utils/dbus_utils.hpp>
+#include <charconv>
+#include <memory>
#include <span>
+#include <string>
namespace redfish
{
@@ -183,6 +190,39 @@
});
}
+inline void doSubscriptionCollection(
+ const boost::system::error_code ec,
+ const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
+ const dbus::utility::ManagedObjectType& resp)
+{
+ if (ec)
+ {
+ if (ec.value() == EBADR)
+ {
+ // This is an optional process so just return if it isn't there
+ return;
+ }
+
+ BMCWEB_LOG_ERROR << "D-Bus response error on GetManagedObjects " << ec;
+ messages::internalError(asyncResp->res);
+ return;
+ }
+ nlohmann::json& memberArray = asyncResp->res.jsonValue["Members"];
+ for (const auto& objpath : resp)
+ {
+ sdbusplus::message::object_path path(objpath.first);
+ const std::string snmpId = path.filename();
+ if (snmpId.empty())
+ {
+ BMCWEB_LOG_ERROR << "The SNMP client ID is wrong";
+ messages::internalError(asyncResp->res);
+ return;
+ }
+
+ getSnmpSubscriptionList(asyncResp, snmpId, memberArray);
+ }
+}
+
inline void requestRoutesEventDestinationCollection(App& app)
{
BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
@@ -210,11 +250,20 @@
for (const std::string& id : subscripIds)
{
nlohmann::json::object_t member;
- member["@odata.id"] = "/redfish/v1/EventService/Subscriptions/" +
- id;
+ member["@odata.id"] = boost::urls::format(
+ "/redfish/v1/EventService/Subscriptions/{}" + id);
memberArray.emplace_back(std::move(member));
}
+ crow::connections::systemBus->async_method_call(
+ [asyncResp](const boost::system::error_code ec,
+ const dbus::utility::ManagedObjectType& resp) {
+ doSubscriptionCollection(ec, asyncResp, resp);
+ },
+ "xyz.openbmc_project.Network.SNMP",
+ "/xyz/openbmc_project/network/snmp/manager",
+ "org.freedesktop.DBus.ObjectManager", "GetManagedObjects");
});
+
BMCWEB_ROUTE(app, "/redfish/v1/EventService/Subscriptions/")
.privileges(redfish::privileges::postEventDestinationCollection)
.methods(boost::beast::http::verb::post)(
@@ -287,10 +336,66 @@
return;
}
+ if (protocol == "SNMPv2c")
+ {
+ if (context)
+ {
+ messages::propertyValueConflict(asyncResp->res, "Context",
+ "Protocol");
+ return;
+ }
+ if (eventFormatType2)
+ {
+ messages::propertyValueConflict(asyncResp->res,
+ "EventFormatType", "Protocol");
+ return;
+ }
+ if (retryPolicy)
+ {
+ messages::propertyValueConflict(asyncResp->res, "RetryPolicy",
+ "Protocol");
+ return;
+ }
+ if (msgIds)
+ {
+ messages::propertyValueConflict(asyncResp->res, "MessageIds",
+ "Protocol");
+ return;
+ }
+ if (regPrefixes)
+ {
+ messages::propertyValueConflict(asyncResp->res,
+ "RegistryPrefixes", "Protocol");
+ return;
+ }
+ if (resTypes)
+ {
+ messages::propertyValueConflict(asyncResp->res, "ResourceTypes",
+ "Protocol");
+ return;
+ }
+ if (headers)
+ {
+ messages::propertyValueConflict(asyncResp->res, "HttpHeaders",
+ "Protocol");
+ return;
+ }
+ if (mrdJsonArray)
+ {
+ messages::propertyValueConflict(
+ asyncResp->res, "MetricReportDefinitions", "Protocol");
+ return;
+ }
+
+ addSnmpTrapClient(asyncResp, host, port);
+ return;
+ }
+
if (path.empty())
{
path = "/";
}
+
std::shared_ptr<Subscription> subValue = std::make_shared<Subscription>(
host, port, path, urlProto, app.ioContext());
@@ -526,6 +631,13 @@
{
return;
}
+
+ if (param.starts_with("snmp"))
+ {
+ getSnmpTrapClient(asyncResp, param);
+ return;
+ }
+
std::shared_ptr<Subscription> subValue =
EventServiceManager::getInstance().getSubscription(param);
if (subValue == nullptr)
@@ -536,7 +648,7 @@
const std::string& id = param;
asyncResp->res.jsonValue["@odata.type"] =
- "#EventDestination.v1_7_0.EventDestination";
+ "#EventDestination.v1_8_0.EventDestination";
asyncResp->res.jsonValue["Protocol"] = "Redfish";
asyncResp->res.jsonValue["@odata.id"] =
"/redfish/v1/EventService/Subscriptions/" + id;
@@ -653,6 +765,14 @@
{
return;
}
+
+ if (param.starts_with("snmp"))
+ {
+ deleteSnmpTrapClient(asyncResp, param);
+ EventServiceManager::getInstance().deleteSubscription(param);
+ return;
+ }
+
if (!EventServiceManager::getInstance().isSubscriptionExist(param))
{
asyncResp->res.result(boost::beast::http::status::not_found);