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/argument.cpp b/src/argument.cpp
index bcd5a13..0c49b11 100644
--- a/src/argument.cpp
+++ b/src/argument.cpp
@@ -72,6 +72,8 @@
     std::cerr << "    --set=<set>       Set a specific package/channel.\n";
     std::cerr
         << "    --clear=<clear>   Clear all the settings on the interface.\n";
+    std::cerr
+        << "    --oem-payload=<hex data> Send an OEM command with payload.\n";
     std::cerr << "    --package=<package>  Specify a package.\n";
     std::cerr << "    --channel=<channel> Specify a channel.\n";
     std::cerr << "    --index=<device index> Specify device ifindex.\n";
@@ -82,6 +84,7 @@
     {"info", no_argument, NULL, 'i'},
     {"set", no_argument, NULL, 's'},
     {"clear", no_argument, NULL, 'r'},
+    {"oem-payload", required_argument, NULL, 'o'},
     {"package", required_argument, NULL, 'p'},
     {"channel", required_argument, NULL, 'c'},
     {"index", required_argument, NULL, 'x'},
@@ -89,7 +92,7 @@
     {0, 0, 0, 0},
 };
 
-const char* ArgumentParser::optionStr = "i:s:r:p:c:x:h?";
+const char* ArgumentParser::optionStr = "i:s:r:o:p:c:x:h?";
 
 const std::string ArgumentParser::trueString = "true";
 const std::string ArgumentParser::emptyString = "";
diff --git a/src/meson.build b/src/meson.build
index 283995a..a2c3758 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -12,6 +12,7 @@
   implicit_include_directories: false,
   include_directories: src_includes,
   dependencies: [
+    dependency('fmt'),
     dependency('libnl-3.0'),
     dependency('libnl-genl-3.0'),
     phosphor_dbus_interfaces_dep,
diff --git a/src/ncsi_netlink_main.cpp b/src/ncsi_netlink_main.cpp
index 9db624f..12d59be 100644
--- a/src/ncsi_netlink_main.cpp
+++ b/src/ncsi_netlink_main.cpp
@@ -18,6 +18,7 @@
 
 #include <iostream>
 #include <string>
+#include <vector>
 
 static void exitWithError(const char* err, char** argv)
 {
@@ -85,8 +86,47 @@
         channelInt = DEFAULT_VALUE;
     }
 
-    auto setCmd = (options)["set"];
-    if (setCmd == "true")
+    auto payloadStr = (options)["oem-payload"];
+    if (!payloadStr.empty())
+    {
+        std::string byte(2, '\0');
+        std::vector<unsigned char> payload;
+
+        if (payloadStr.size() % 2)
+            exitWithError("Payload invalid: specify two hex digits per byte.",
+                          argv);
+
+        // Parse the payload string (e.g. "000001572100") to byte data
+        for (unsigned int i = 1; i < payloadStr.size(); i += 2)
+        {
+            byte[0] = payloadStr[i - 1];
+            byte[1] = payloadStr[i];
+
+            try
+            {
+                payload.push_back(stoi(byte, nullptr, 16));
+            }
+            catch (const std::exception& e)
+            {
+                exitWithError("Payload invalid.", argv);
+            }
+        }
+
+        if (payload.empty())
+        {
+            exitWithError("No payload specified.", argv);
+        }
+
+        if (packageInt == DEFAULT_VALUE)
+        {
+            exitWithError("Package not specified.", argv);
+        }
+
+        return ncsi::sendOemCommand(
+            indexInt, packageInt, channelInt,
+            std::span<const unsigned char>(payload.begin(), payload.end()));
+    }
+    else if ((options)["set"] == "true")
     {
         // Can not perform set operation without package.
         if (packageInt == DEFAULT_VALUE)
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
     {
diff --git a/src/ncsi_util.hpp b/src/ncsi_util.hpp
index db754fe..eaa076d 100644
--- a/src/ncsi_util.hpp
+++ b/src/ncsi_util.hpp
@@ -1,3 +1,7 @@
+#pragma once
+
+#include <span>
+
 namespace phosphor
 {
 namespace network
@@ -9,6 +13,20 @@
 constexpr auto NONE = 0;
 
 /* @brief  This function will ask underlying NCSI driver
+ *         to send an OEM command (command type 0x50) with
+ *         the specified payload as the OEM data.
+ *         This function talks with the NCSI driver over
+ *         netlink messages.
+ * @param[in] ifindex - Interface Index.
+ * @param[in] package - NCSI Package.
+ * @param[in] channel - Channel number with in the package.
+ * @param[in] payload - OEM data to send.
+ * @returns 0 on success and negative value for failure.
+ */
+int sendOemCommand(int ifindex, int package, int channel,
+                   std::span<const unsigned char> payload);
+
+/* @brief  This function will ask underlying NCSI driver
  *         to set a specific  package or package/channel
  *         combination as the preferred choice.
  *         This function talks with the NCSI driver over