Add support for sending NCSI command

Provide a means to send an OEM command to an NIC via NCSI netlink.
This may be invoked from a systemd Unit file to configure NIC
behavior.

Some NICs provide OEM commands to influence their behavior, for
example maintaining full speed even when the host is down instead
of negotiating a lower speed for power.

Signed-off-by: Eddie James <eajames@linux.ibm.com>
Change-Id: Id920b618422e8fbfc51984fbf932045bfb5e56e6
diff --git a/src/ncsi_util.cpp b/src/ncsi_util.cpp
index 0436987..ec6074f 100644
--- a/src/ncsi_util.cpp
+++ b/src/ncsi_util.cpp
@@ -1,10 +1,12 @@
 #include "ncsi_util.hpp"
 
+#include <fmt/format.h>
 #include <linux/ncsi.h>
 #include <netlink/genl/ctrl.h>
 #include <netlink/genl/genl.h>
 #include <netlink/netlink.h>
 
+#include <iomanip>
 #include <iostream>
 #include <phosphor-logging/elog-errors.hpp>
 #include <phosphor-logging/log.hpp>
@@ -25,10 +27,44 @@
 namespace internal
 {
 
+struct NCSIPacketHeader
+{
+    uint8_t MCID;
+    uint8_t revision;
+    uint8_t reserved;
+    uint8_t id;
+    uint8_t type;
+    uint8_t channel;
+    uint16_t length;
+    uint32_t rsvd[2];
+};
+
+class Command
+{
+  public:
+    Command() = delete;
+    ~Command() = default;
+    Command(const Command&) = delete;
+    Command& operator=(const Command&) = delete;
+    Command(Command&&) = default;
+    Command& operator=(Command&&) = default;
+    Command(
+        int c, int nc = DEFAULT_VALUE,
+        std::span<const unsigned char> p = std::span<const unsigned char>()) :
+        cmd(c),
+        ncsi_cmd(nc), payload(p)
+    {
+    }
+
+    int cmd;
+    int ncsi_cmd;
+    std::span<const unsigned char> payload;
+};
+
 using nlMsgPtr = std::unique_ptr<nl_msg, decltype(&::nlmsg_free)>;
 using nlSocketPtr = std::unique_ptr<nl_sock, decltype(&::nl_socket_free)>;
 
-CallBack infoCallBack = [](struct nl_msg* msg, void* /*arg*/) {
+CallBack infoCallBack = [](struct nl_msg* msg, void* arg) {
     using namespace phosphor::network::ncsi;
     auto nlh = nlmsg_hdr(msg);
 
@@ -50,6 +86,8 @@
         {NLA_FLAG, 0, 0},   {NLA_NESTED, 0, 0}, {NLA_UNSPEC, 0, 0},
     };
 
+    *(int*)arg = 0;
+
     auto ret = genlmsg_parse(nlh, 0, tb, NCSI_ATTR_MAX, ncsiPolicy);
     if (!tb[NCSI_ATTR_PACKAGE_LIST])
     {
@@ -175,10 +213,43 @@
     return (int)NL_SKIP;
 };
 
-int applyCmd(int ifindex, int cmd, int package = DEFAULT_VALUE,
+CallBack sendCallBack = [](struct nl_msg* msg, void* arg) {
+    using namespace phosphor::network::ncsi;
+    auto nlh = nlmsg_hdr(msg);
+    struct nlattr* tb[NCSI_ATTR_MAX + 1] = {nullptr};
+    static struct nla_policy ncsiPolicy[NCSI_ATTR_MAX + 1] = {
+        {NLA_UNSPEC, 0, 0}, {NLA_U32, 0, 0}, {NLA_NESTED, 0, 0},
+        {NLA_U32, 0, 0},    {NLA_U32, 0, 0}, {NLA_BINARY, 0, 0},
+        {NLA_FLAG, 0, 0},   {NLA_U32, 0, 0}, {NLA_U32, 0, 0},
+    };
+
+    *(int*)arg = 0;
+
+    auto ret = genlmsg_parse(nlh, 0, tb, NCSI_ATTR_MAX, ncsiPolicy);
+    if (ret)
+    {
+        std::cerr << "Failed to parse package" << std::endl;
+        return ret;
+    }
+
+    auto data_len = nla_len(tb[NCSI_ATTR_DATA]) - sizeof(NCSIPacketHeader);
+    unsigned char* data =
+        (unsigned char*)nla_data(tb[NCSI_ATTR_DATA]) + sizeof(NCSIPacketHeader);
+    auto s = std::span<const unsigned char>(data, data_len);
+
+    // Dump the response to stdout. Enhancement: option to save response data
+    std::cout << "Response : " << std::dec << data_len << " bytes" << std::endl;
+    fmt::print("{:02x}", fmt::join(s.begin(), s.end(), " "));
+    std::cout << std::endl;
+
+    return 0;
+};
+
+int applyCmd(int ifindex, const Command& cmd, int package = DEFAULT_VALUE,
              int channel = DEFAULT_VALUE, int flags = NONE,
              CallBack function = nullptr)
 {
+    int cb_ret = 0;
     nlSocketPtr socket(nl_socket_alloc(), &::nl_socket_free);
     if (socket == nullptr)
     {
@@ -207,10 +278,10 @@
         return -ENOMEM;
     }
 
-    auto msgHdr = genlmsg_put(msg.get(), 0, 0, driverID, 0, flags, cmd, 0);
+    auto msgHdr = genlmsg_put(msg.get(), 0, 0, driverID, 0, flags, cmd.cmd, 0);
     if (!msgHdr)
     {
-        std::cerr << "Unable to add the netlink headers , COMMAND : " << cmd
+        std::cerr << "Unable to add the netlink headers , COMMAND : " << cmd.cmd
                   << std::endl;
         return -ENOMEM;
     }
@@ -247,11 +318,37 @@
         return ret;
     }
 
+    if (cmd.ncsi_cmd != DEFAULT_VALUE)
+    {
+        std::vector<unsigned char> pl(sizeof(NCSIPacketHeader) +
+                                      cmd.payload.size());
+        NCSIPacketHeader* hdr = (NCSIPacketHeader*)pl.data();
+
+        std::copy(cmd.payload.begin(), cmd.payload.end(),
+                  pl.begin() + sizeof(NCSIPacketHeader));
+
+        hdr->type = cmd.ncsi_cmd;
+        hdr->length = htons(cmd.payload.size());
+
+        ret = nla_put(msg.get(), ncsi_nl_attrs::NCSI_ATTR_DATA, pl.size(),
+                      pl.data());
+        if (ret < 0)
+        {
+            std::cerr << "Failed to set the data attribute, RC : " << ret
+                      << std::endl;
+            return ret;
+        }
+
+        nl_socket_disable_seq_check(socket.get());
+    }
+
     if (function)
     {
+        cb_ret = 1;
+
         // Add a callback function to the socket
         nl_socket_modify_cb(socket.get(), NL_CB_VALID, NL_CB_CUSTOM, function,
-                            nullptr);
+                            &cb_ret);
     }
 
     ret = nl_send_auto(socket.get(), msg.get());
@@ -261,32 +358,63 @@
         return ret;
     }
 
-    ret = nl_recvmsgs_default(socket.get());
-    if (ret < 0)
+    do
     {
-        std::cerr << "Failed to receive the message , RC : " << ret
-                  << std::endl;
-    }
+        ret = nl_recvmsgs_default(socket.get());
+        if (ret < 0)
+        {
+            std::cerr << "Failed to receive the message , RC : " << ret
+                      << std::endl;
+            break;
+        }
+    } while (cb_ret);
+
     return ret;
 }
 
 } // namespace internal
 
+int sendOemCommand(int ifindex, int package, int channel,
+                   std::span<const unsigned char> payload)
+{
+    constexpr auto cmd = 0x50;
+
+    std::cout << "Send OEM Command, CHANNEL : " << std::hex << channel
+              << ", PACKAGE : " << std::hex << package
+              << ", IFINDEX: " << std::hex << ifindex << std::endl;
+    if (!payload.empty())
+    {
+        std::cout << "Payload :";
+        for (auto& i : payload)
+        {
+            std::cout << " " << std::hex << std::setfill('0') << std::setw(2)
+                      << (int)i;
+        }
+        std::cout << std::endl;
+    }
+
+    return internal::applyCmd(
+        ifindex,
+        internal::Command(ncsi_nl_commands::NCSI_CMD_SEND_CMD, cmd, payload),
+        package, channel, NONE, internal::sendCallBack);
+}
+
 int setChannel(int ifindex, int package, int channel)
 {
     std::cout << "Set Channel : " << std::hex << channel
               << ", PACKAGE : " << std::hex << package
               << ", IFINDEX :  " << std::hex << ifindex << std::endl;
-    return internal::applyCmd(ifindex, ncsi_nl_commands::NCSI_CMD_SET_INTERFACE,
-                              package, channel);
+    return internal::applyCmd(
+        ifindex, internal::Command(ncsi_nl_commands::NCSI_CMD_SET_INTERFACE),
+        package, channel);
 }
 
 int clearInterface(int ifindex)
 {
     std::cout << "ClearInterface , IFINDEX :" << std::hex << ifindex
               << std::endl;
-    return internal::applyCmd(ifindex,
-                              ncsi_nl_commands::NCSI_CMD_CLEAR_INTERFACE);
+    return internal::applyCmd(
+        ifindex, internal::Command(ncsi_nl_commands::NCSI_CMD_CLEAR_INTERFACE));
 }
 
 int getInfo(int ifindex, int package)
@@ -295,9 +423,9 @@
               << ", IFINDEX :  " << std::hex << ifindex << std::endl;
     if (package == DEFAULT_VALUE)
     {
-        return internal::applyCmd(ifindex, ncsi_nl_commands::NCSI_CMD_PKG_INFO,
-                                  package, DEFAULT_VALUE, NLM_F_DUMP,
-                                  internal::infoCallBack);
+        return internal::applyCmd(
+            ifindex, internal::Command(ncsi_nl_commands::NCSI_CMD_PKG_INFO),
+            package, DEFAULT_VALUE, NLM_F_DUMP, internal::infoCallBack);
     }
     else
     {