ncsi: return interface info from Interface::getInfo()

Rather than expecting the ncsi_util callback to print the interface
info, return the interface info, and print that.

This allows for a specific output formatting function in
ncsi_netlink_main.c, rather than printing directly from the libnl
callback. We reformat for a more structured display of the
package/channel layouts.

We use std::optional for the return value here; it would be nice to use
std::expected instead, and get a full error code, but that's not quite
working with clang-18 at present. Even if we have an error code though,
we're just going to return EXIT_FAILURE anyway.

Tested: Invoked on a simlated dual-channel NCSI package:

    # ncsi-netlink -x 2 --info
    <7> Get Info , PACKAGE : 0xffffffffffffffff, INTERFACE: 0x7e8bf9bc
    <7> Package id : 0
    <7>   package is forced
    <7>     Channel id : 0
    <7>       version 1.2 (p0c00)
    <7>       link state 0x40022f
    <7>     Channel id : 1
    <7>       version 1.2 (p0c01)
    <7>       link state 0x40022f

Change-Id: Idb62cc6695da67f4415ed9b0e7950c506018d630
Signed-off-by: Jeremy Kerr <jk@codeconstruct.com.au>
diff --git a/src/ncsi_netlink_main.cpp b/src/ncsi_netlink_main.cpp
index 04e0cbc..d5c6fc5 100644
--- a/src/ncsi_netlink_main.cpp
+++ b/src/ncsi_netlink_main.cpp
@@ -28,6 +28,50 @@
     exit(EXIT_FAILURE);
 }
 
+static void printInfo(phosphor::network::ncsi::InterfaceInfo& info)
+{
+    using namespace phosphor::network::ncsi;
+
+    for (PackageInfo& pkg : info.packages)
+    {
+        lg2::debug("Package id : {ID}", "ID", pkg.id);
+        if (pkg.forced)
+        {
+            lg2::debug("  package is forced");
+        }
+        for (ChannelInfo& chan : pkg.channels)
+        {
+            lg2::debug("    Channel id : {ID}", "ID", chan.id);
+            if (chan.forced)
+            {
+                lg2::debug("    channel is forced");
+            }
+            if (chan.active)
+            {
+                lg2::debug("    channel is active");
+            }
+
+            lg2::debug("      version {MAJOR}.{MINOR} ({STR})", "MAJOR",
+                       chan.version_major, "MINOR", chan.version_minor, "STR",
+                       chan.version);
+
+            lg2::debug("      link state {LINK}", "LINK", lg2::hex,
+                       chan.link_state);
+
+            auto& vlans = chan.vlan_ids;
+
+            if (!vlans.empty())
+            {
+                lg2::debug("      Actve VLAN IDs:");
+                for (uint16_t vlan : vlans)
+                {
+                    lg2::debug("        VID: {VLAN_ID}", "VLAN_ID", vlan);
+                }
+            }
+        }
+    }
+}
+
 int main(int argc, char** argv)
 {
     using namespace phosphor::network;
@@ -153,7 +197,12 @@
     }
     else if ((options)["info"] == "true")
     {
-        return interface.getInfo(packageInt);
+        auto info = interface.getInfo(packageInt);
+        if (!info)
+        {
+            return EXIT_FAILURE;
+        }
+        printInfo(*info);
     }
     else if ((options)["clear"] == "true")
     {
diff --git a/src/ncsi_util.cpp b/src/ncsi_util.cpp
index c755d33..8c628aa 100644
--- a/src/ncsi_util.cpp
+++ b/src/ncsi_util.cpp
@@ -78,7 +78,19 @@
 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*) {
+struct infoCallBackContext
+{
+    InterfaceInfo* info;
+};
+
+CallBack infoCallBack = [](struct nl_msg* msg, void* arg) {
+    if (arg == nullptr)
+    {
+        lg2::error("Internal error: invalid info callback context");
+        return -1;
+    }
+
+    struct infoCallBackContext* info = (struct infoCallBackContext*)arg;
     using namespace phosphor::network::ncsi;
     auto nlh = nlmsg_hdr(msg);
 
@@ -125,11 +137,12 @@
             return -1;
         }
 
+        PackageInfo pkg;
+
         if (packagetb[NCSI_PKG_ATTR_ID])
         {
             auto attrID = nla_get_u32(packagetb[NCSI_PKG_ATTR_ID]);
-            lg2::debug("Package has id : {ATTR_ID}", "ATTR_ID", lg2::hex,
-                       attrID);
+            pkg.id = attrID;
         }
         else
         {
@@ -138,7 +151,7 @@
 
         if (packagetb[NCSI_PKG_ATTR_FORCED])
         {
-            lg2::debug("This package is forced");
+            pkg.forced = true;
         }
 
         auto channelListTarget = static_cast<nlattr*>(
@@ -153,75 +166,59 @@
             if (ret < 0)
             {
                 lg2::error("Failed to parse channel nested");
-                return -1;
+                continue;
             }
 
+            ChannelInfo chan;
+
             if (channeltb[NCSI_CHANNEL_ATTR_ID])
             {
-                auto channel = nla_get_u32(channeltb[NCSI_CHANNEL_ATTR_ID]);
-                if (channeltb[NCSI_CHANNEL_ATTR_ACTIVE])
-                {
-                    lg2::debug("Channel Active : {CHANNEL}", "CHANNEL",
-                               lg2::hex, channel);
-                }
-                else
-                {
-                    lg2::debug("Channel Not Active : {CHANNEL}", "CHANNEL",
-                               lg2::hex, channel);
-                }
-
-                if (channeltb[NCSI_CHANNEL_ATTR_FORCED])
-                {
-                    lg2::debug("Channel is forced");
-                }
+                chan.id = nla_get_u32(channeltb[NCSI_CHANNEL_ATTR_ID]);
+                chan.active = !!channeltb[NCSI_CHANNEL_ATTR_ACTIVE];
+                chan.forced = !!channeltb[NCSI_CHANNEL_ATTR_FORCED];
             }
             else
             {
                 lg2::debug("Channel with no ID");
+                continue;
             }
 
             if (channeltb[NCSI_CHANNEL_ATTR_VERSION_MAJOR])
             {
-                auto major =
+                chan.version_major =
                     nla_get_u32(channeltb[NCSI_CHANNEL_ATTR_VERSION_MAJOR]);
-                lg2::debug("Channel Major Version : {CHANNEL_MAJOR_VERSION}",
-                           "CHANNEL_MAJOR_VERSION", lg2::hex, major);
             }
             if (channeltb[NCSI_CHANNEL_ATTR_VERSION_MINOR])
             {
-                auto minor =
+                chan.version_minor =
                     nla_get_u32(channeltb[NCSI_CHANNEL_ATTR_VERSION_MINOR]);
-                lg2::debug("Channel Minor Version : {CHANNEL_MINOR_VERSION}",
-                           "CHANNEL_MINOR_VERSION", lg2::hex, minor);
             }
             if (channeltb[NCSI_CHANNEL_ATTR_VERSION_STR])
             {
-                auto str =
+                chan.version =
                     nla_get_string(channeltb[NCSI_CHANNEL_ATTR_VERSION_STR]);
-                lg2::debug("Channel Version Str : {CHANNEL_VERSION_STR}",
-                           "CHANNEL_VERSION_STR", str);
             }
             if (channeltb[NCSI_CHANNEL_ATTR_LINK_STATE])
             {
-                auto link =
+                chan.link_state =
                     nla_get_u32(channeltb[NCSI_CHANNEL_ATTR_LINK_STATE]);
-                lg2::debug("Channel Link State : {LINK_STATE}", "LINK_STATE",
-                           lg2::hex, link);
             }
             if (channeltb[NCSI_CHANNEL_ATTR_VLAN_LIST])
             {
-                lg2::debug("Active Vlan ids");
                 auto vids = channeltb[NCSI_CHANNEL_ATTR_VLAN_LIST];
                 auto vid = static_cast<nlattr*>(nla_data(vids));
                 auto len = nla_len(vids);
                 while (nla_ok(vid, len))
                 {
                     auto id = nla_get_u16(vid);
-                    lg2::debug("VID : {VLAN_ID}", "VLAN_ID", id);
+                    chan.vlan_ids.push_back(id);
                     vid = nla_next(vid, &len);
                 }
             }
+            pkg.channels.push_back(chan);
         }
+
+        info->info->packages.push_back(pkg);
     }
     return static_cast<int>(NL_STOP);
 };
@@ -263,7 +260,7 @@
 
 int applyCmd(Interface& interface, const Command& cmd,
              int package = DEFAULT_VALUE, int channel = DEFAULT_VALUE,
-             int flags = NONE, CallBack function = nullptr)
+             int flags = NONE, CallBack function = nullptr, void* arg = nullptr)
 {
     nlSocketPtr socket(nl_socket_alloc(), &::nl_socket_free);
     if (socket == nullptr)
@@ -388,7 +385,7 @@
 
     // Add a callback function to the socket
     enum nl_cb_kind cb_kind = function ? NL_CB_CUSTOM : NL_CB_DEFAULT;
-    nl_socket_modify_cb(socket.get(), NL_CB_VALID, cb_kind, function, nullptr);
+    nl_socket_modify_cb(socket.get(), NL_CB_VALID, cb_kind, function, arg);
 
     ret = nl_send_auto(socket.get(), msg.get());
     if (ret < 0)
@@ -451,22 +448,28 @@
         *this, internal::Command(ncsi_nl_commands::NCSI_CMD_CLEAR_INTERFACE));
 }
 
-int Interface::getInfo(int package)
+std::optional<InterfaceInfo> Interface::getInfo(int package)
 {
+    int rc, flags = package == DEFAULT_VALUE ? NLM_F_DUMP : NONE;
+    InterfaceInfo info;
+
     lg2::debug("Get Info , PACKAGE : {PACKAGE}, INTERFACE: {INTERFACE}",
                "PACKAGE", lg2::hex, package, "INTERFACE", this);
-    if (package == DEFAULT_VALUE)
+
+    struct internal::infoCallBackContext ctx = {
+        .info = &info,
+    };
+
+    rc = internal::applyCmd(
+        *this, internal::Command(ncsi_nl_commands::NCSI_CMD_PKG_INFO), package,
+        DEFAULT_VALUE, flags, internal::infoCallBack, &ctx);
+
+    if (rc < 0)
     {
-        return internal::applyCmd(
-            *this, internal::Command(ncsi_nl_commands::NCSI_CMD_PKG_INFO),
-            package, DEFAULT_VALUE, NLM_F_DUMP, internal::infoCallBack);
+        return {};
     }
-    else
-    {
-        return internal::applyCmd(*this, ncsi_nl_commands::NCSI_CMD_PKG_INFO,
-                                  package, DEFAULT_VALUE, NONE,
-                                  internal::infoCallBack);
-    }
+
+    return info;
 }
 
 int Interface::setPackageMask(unsigned int mask)
diff --git a/src/ncsi_util.hpp b/src/ncsi_util.hpp
index aa2051c..452c916 100644
--- a/src/ncsi_util.hpp
+++ b/src/ncsi_util.hpp
@@ -1,7 +1,11 @@
 #pragma once
 
+#include <stdint.h>
+
+#include <optional>
 #include <span>
 #include <string>
+#include <vector>
 
 namespace phosphor
 {
@@ -13,6 +17,29 @@
 constexpr auto DEFAULT_VALUE = -1;
 constexpr auto NONE = 0;
 
+struct ChannelInfo
+{
+    uint32_t id;
+    bool active;
+    bool forced;
+    uint32_t version_major, version_minor;
+    std::string version;
+    uint32_t link_state;
+    std::vector<uint16_t> vlan_ids;
+};
+
+struct PackageInfo
+{
+    uint32_t id;
+    bool forced;
+    std::vector<ChannelInfo> channels;
+};
+
+struct InterfaceInfo
+{
+    std::vector<PackageInfo> packages;
+};
+
 struct Interface
 {
     /* @brief  This function will ask underlying NCSI driver
@@ -50,11 +77,13 @@
 
     /* @brief  This function is used to dump all the info
      *         of the package and the channels underlying
-     *         the package.
-     * @param[in] package - NCSI Package.
-     * @returns 0 on success and negative value for failure.
+     *         the package, or all packages if DEFAULT_VALUE
+     *         is passed
+     * @param[in] package - NCSI Package
+     * @returns an InterfaceInfo with package data the specified pacakge,
+     *          or all packages if none is specified.
      */
-    int getInfo(int package);
+    std::optional<InterfaceInfo> getInfo(int package);
 
     /* @brief  This function assigns a mask controlling responses to AEN from a
      * package.