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/http/utility.hpp b/http/utility.hpp
index aa64043..3fbdd0c 100644
--- a/http/utility.hpp
+++ b/http/utility.hpp
@@ -551,6 +551,10 @@
         }
         return "";
     }
+    if (urlView.scheme() == "snmp")
+    {
+        return "snmp";
+    }
     return "";
 }
 
@@ -573,6 +577,10 @@
     {
         return 443;
     }
+    if (url.scheme() == "snmp")
+    {
+        return 162;
+    }
     return 0;
 }
 
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);