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/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);