ncsi-cmd: Add a new executable for issuing NCSI commands
At the moment, the ncsi-netlink utility does two things:
- allows management of the kernel NCSI state, such as the channel and
package masks
- allows issuing NCSI messages to the NCSI-capable NIC, through the
NCSI_CMD_SEND_CMD interface
While these two things do share the same kernel API, they have somewhat
different objectives: one is controlling local state, the other is
controlling remote (ie, the NIC) state.
In future, we want to allow non-netlink-based NCSI transports for
issuing commands to the NIC, which makes the ncsi-netlink name somewhat
inaccurate for those.
So, introduce a new tool, 'ncsi-cmd', for issuing NCSI commands over the
netlink interface.
This has similar command-line semantics to the existing
'ncsi-netlink [...] -o <PAYLOAD>' usage, but has a few changes for a
more ergonomic UI:
Firstly, the type (or "opcode") byte is no longer packed into the
payload data, because it's not really payload.
Secondly, we use --interface/-i rather than --index/-x, with a note that
interfaces are specified by index. This allows for future changes that
allow specifying interfaces by name.
Finally, to make it clear that we can issue more than just OEM
commands, we have separate subcommands: "oem" and "raw". These are
similar, just that "oem" implies the standard OEM type value of 0x50.
So, the following are equivalent:
ncsi-cmd -i2 -p0 oem 010203
ncsi-cmd -i2 -p0 raw 0x50 010203
But now we have a cleaner interface for not-OEM commands:
ncsi-cmd -i12 -p0 raw 0x15
For issuing command type 0x15, "Get Version ID".
We remove the send logic from ncsi-netlink, but leave a compatibility
shim that will exec() ncsi-cmd with the appropriate arguments instead.
Change-Id: Ied240db0d545d5770df0927da354c65b82ee9508
Signed-off-by: Jeremy Kerr <jk@codeconstruct.com.au>
diff --git a/src/argument.cpp b/src/argument.cpp
index 16d855c..6d1273f 100644
--- a/src/argument.cpp
+++ b/src/argument.cpp
@@ -73,7 +73,6 @@
" --info | -i Retrieve info about NCSI topology.\n"
" --set | -s Set a specific package/channel.\n"
" --clear | -r Clear all the settings on the interface.\n"
- " --oem-payload=<hex data...> | -o <hex data...> Send an OEM command with payload.\n"
" --pmask=<mask> | -j <mask> Bitmask to enable/disable packages\n"
" --cmask=<mask> | -k <mask> Bitmask to enable/disable channels\n"
"\n"
@@ -86,11 +85,9 @@
" ncsi-netlink -x 3 -p 0 -c 1 -s\n"
" 4) Clear preferred channel\n"
" ncsi-netlink -x 3 -p 0 -r\n"
- " 5) Send NCSI Command\n"
- " ncsi-netlink -x 3 -p 0 -c 0 -o 50000001572100\n"
- " 6) Set Package Mask\n"
+ " 5) Set Package Mask\n"
" ncsi-netlink -x 3 -j 1\n"
- " 7) Set Channel Mask\n"
+ " 6) Set Channel Mask\n"
" ncsi-netlink -x 3 -p 0 -k 1\n"
"\n";
}
diff --git a/src/meson.build b/src/meson.build
index 1a5084b..01eb1eb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -18,6 +18,21 @@
install: true,
install_dir: get_option('bindir'))
+executable(
+ 'ncsi-cmd',
+ 'ncsi_cmd.cpp',
+ 'ncsi_util.cpp',
+ implicit_include_directories: false,
+ include_directories: src_includes,
+ dependencies: [
+ dependency('libnl-3.0'),
+ dependency('libnl-genl-3.0'),
+ dependency('phosphor-logging'),
+ stdplus_dep,
+ ],
+ install: true,
+ install_dir: get_option('bindir'))
+
main_deps = []
main_srcs = []
if get_option('sync-mac')
diff --git a/src/ncsi_cmd.cpp b/src/ncsi_cmd.cpp
new file mode 100644
index 0000000..07a8d7e
--- /dev/null
+++ b/src/ncsi_cmd.cpp
@@ -0,0 +1,391 @@
+/**
+ * Copyright © 2018 IBM Corporation
+ * Copyright © 2024 Code Construct
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "ncsi_util.hpp"
+
+#include <assert.h>
+#include <getopt.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <phosphor-logging/lg2.hpp>
+#include <stdplus/numeric/str.hpp>
+#include <stdplus/str/buf.hpp>
+#include <stdplus/str/conv.hpp>
+
+#include <climits>
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+using namespace phosphor::network::ncsi;
+
+struct GlobalOptions
+{
+ std::unique_ptr<Interface> interface;
+ unsigned int package;
+ std::optional<unsigned int> channel;
+};
+
+const struct option options[] = {
+ {"package", required_argument, NULL, 'p'},
+ {"channel", required_argument, NULL, 'c'},
+ {"interface", required_argument, NULL, 'i'},
+ {"help", no_argument, NULL, 'h'},
+ {0, 0, 0, 0},
+};
+
+static void print_usage(const char* progname)
+{
+ // clang-format off
+ std::cerr
+ << "Usage:\n"
+ " " << progname << " <options> raw TYPE [PAYLOAD...]\n"
+ " " << progname << " <options> oem [PAYLOAD...]\n"
+ "\n"
+ "Global options:\n"
+ " --interface IFACE, -i Specify net device by ifindex.\n"
+ " --package PACKAGE, -p Specify a package.\n"
+ " --channel CHANNEL, -c Specify a channel.\n"
+ "\n"
+ "Both --interface/-i and --package/-p are required.\n"
+ "\n"
+ "Subcommands:\n"
+ "\n"
+ "raw TYPE [PAYLOAD...]\n"
+ " Send a single command using raw type/payload data.\n"
+ " TYPE NC-SI command type, in hex\n"
+ " PAYLOAD Command payload bytes, as hex\n"
+ "\n"
+ "oem PAYLOAD\n"
+ " Send a single OEM command (type 0x50).\n"
+ " PAYLOAD Command payload bytes, as hex\n";
+ // clang-format on
+}
+
+static std::optional<unsigned int>
+ parseUnsigned(const char* str, const char* label)
+{
+ try
+ {
+ unsigned long tmp = std::stoul(str, NULL, 16);
+ if (tmp <= UINT_MAX)
+ return tmp;
+ }
+ catch (const std::exception& e)
+ {}
+ std::cerr << "Invalid " << label << " argument '" << str << "'\n";
+ return {};
+}
+
+static std::optional<std::vector<unsigned char>>
+ parsePayload(int argc, const char* const argv[])
+{
+ /* we have already checked that there are sufficient args in callers */
+ assert(argc >= 1);
+
+ std::vector<unsigned char> payload;
+
+ /* we support two formats of payload - all as one argument:
+ * 00010c202f
+ *
+ * or single bytes in separate arguments:
+ * 00 01 0c 20 2f
+ *
+ * both are assumed as entirely hex, but the latter format does not
+ * need to be exactly two chars per byte:
+ * 0 1 c 20 2f
+ */
+
+ size_t len0 = strlen(argv[0]);
+ if (argc == 1 && len0 > 2)
+ {
+ /* single argument format, parse as multiple bytes */
+ if (len0 % 2 != 0)
+ {
+ std::cerr << "Invalid payload length " << len0
+ << " (must be a multiple of 2 chars)\n";
+ return {};
+ }
+
+ std::string str(argv[0]);
+ std::string_view sv(str);
+
+ for (unsigned int i = 0; i < sv.size(); i += 2)
+ {
+ unsigned char byte;
+ auto begin = sv.data() + i;
+ auto end = begin + 2;
+
+ auto [next, err] = std::from_chars(begin, end, byte, 16);
+
+ if (err != std::errc() || next != end)
+ {
+ std::cerr << "Invalid payload string\n";
+ return {};
+ }
+ payload.push_back(byte);
+ }
+ }
+ else
+ {
+ /* multiple payload arguments, each is a separate hex byte */
+ for (int i = 0; i < argc; i++)
+ {
+ unsigned char byte;
+ auto begin = argv[i];
+ auto end = begin + strlen(begin);
+
+ auto [next, err] = std::from_chars(begin, end, byte, 16);
+
+ if (err != std::errc() || next != end)
+ {
+ std::cerr << "Invalid payload argument '" << begin << "'\n";
+ return {};
+ }
+ payload.push_back(byte);
+ }
+ }
+
+ return payload;
+}
+
+static std::optional<std::tuple<GlobalOptions, int>>
+ parseGlobalOptions(int argc, char* const* argv)
+{
+ std::optional<unsigned int> chan, package, interface;
+ const char* progname = argv[0];
+ GlobalOptions opts{};
+
+ for (;;)
+ {
+ /* We're using + here as we want to stop parsing at the subcommand
+ * name
+ */
+ int opt = getopt_long(argc, argv, "+p:c:i:h", options, NULL);
+ if (opt == -1)
+ {
+ break;
+ }
+
+ switch (opt)
+ {
+ case 'i':
+ interface = parseUnsigned(optarg, "interface");
+ if (!interface.has_value())
+ {
+ return {};
+ }
+ break;
+
+ case 'p':
+ package = parseUnsigned(optarg, "package");
+ if (!package.has_value())
+ {
+ return {};
+ }
+ break;
+
+ case 'c':
+ chan = parseUnsigned(optarg, "channel");
+ if (!chan.has_value())
+ {
+ return {};
+ }
+ opts.channel = *chan;
+ break;
+
+ case 'h':
+ default:
+ print_usage(progname);
+ return {};
+ }
+ }
+
+ if (!interface.has_value())
+ {
+ std::cerr << "Missing interface, add an --interface argument\n";
+ return {};
+ }
+
+ if (!package.has_value())
+ {
+ std::cerr << "Missing package, add a --package argument\n";
+ return {};
+ }
+
+ opts.interface = std::make_unique<NetlinkInterface>(*interface);
+ opts.package = *package;
+
+ return std::make_tuple(std::move(opts), optind);
+}
+
+static stdplus::StrBuf toHexStr(std::span<const uint8_t> c) noexcept
+{
+ stdplus::StrBuf ret;
+ if (c.empty())
+ {
+ /* workaround for lg2's handling of string_view */
+ *ret.data() = '\0';
+ return ret;
+ }
+ stdplus::IntToStr<16, uint8_t> its;
+ auto oit = ret.append(c.size() * 3);
+ auto cit = c.begin();
+ oit = its(oit, *cit++, 2);
+ for (; cit != c.end(); ++cit)
+ {
+ *oit++ = ' ';
+ oit = its(oit, *cit, 2);
+ }
+ *oit = 0;
+ return ret;
+}
+
+/* Helper for the 'raw' and 'oem' command handlers: Construct a single command,
+ * issue it to the interface, and print the resulting response payload.
+ */
+static int ncsiCommand(GlobalOptions& options, uint8_t type,
+ std::vector<unsigned char> payload)
+{
+ NCSICommand cmd(type, options.package, options.channel, payload);
+
+ lg2::debug("Command: type {TYPE}, payload {PAYLOAD_LEN} bytes: {PAYLOAD}",
+ "TYPE", lg2::hex, type, "PAYLOAD_LEN", payload.size(), "PAYLOAD",
+ toHexStr(payload));
+
+ auto resp = options.interface->sendCommand(cmd);
+ if (!resp)
+ {
+ return -1;
+ }
+
+ lg2::debug("Response {DATA_LEN} bytes: {DATA}", "DATA_LEN",
+ resp->full_payload.size(), "DATA", toHexStr(resp->full_payload));
+
+ return 0;
+}
+
+static int ncsiCommandRaw(GlobalOptions& options, int argc,
+ const char* const* argv)
+{
+ std::vector<unsigned char> payload;
+ std::optional<uint8_t> type;
+
+ if (argc < 2)
+ {
+ std::cerr << "Invalid arguments for 'raw' subcommand\n";
+ return -1;
+ }
+
+ /* Not only does the type need to fit into one byte, but the top bit
+ * is used for the request/response flag, so check for 0x80 here as
+ * our max here.
+ */
+ type = parseUnsigned(argv[1], "command type");
+ if (!type.has_value() || *type > 0x80)
+ {
+ std::cerr << "Invalid command type value\n";
+ return -1;
+ }
+
+ if (argc >= 3)
+ {
+ auto tmp = parsePayload(argc - 2, argv + 2);
+ if (!tmp.has_value())
+ {
+ return -1;
+ }
+
+ payload = *tmp;
+ }
+
+ return ncsiCommand(options, *type, payload);
+}
+
+static int ncsiCommandOEM(GlobalOptions& options, int argc,
+ const char* const* argv)
+{
+ constexpr uint8_t oemType = 0x50;
+
+ if (argc < 2)
+ {
+ std::cerr << "Invalid arguments for 'oem' subcommand\n";
+ return -1;
+ }
+
+ auto payload = parsePayload(argc - 1, argv + 1);
+ if (!payload.has_value())
+ {
+ return -1;
+ }
+
+ return ncsiCommand(options, oemType, *payload);
+}
+
+/* A note on log output:
+ * For output that relates to command-line usage, we just output directly to
+ * stderr. Once we have a properly parsed command line invocation, we use lg2
+ * for log output, as we want that to use the standard log facilities to
+ * catch runtime error scenarios
+ */
+int main(int argc, char** argv)
+{
+ const char* progname = argv[0];
+
+ auto opts = parseGlobalOptions(argc, argv);
+
+ if (!opts.has_value())
+ {
+ return EXIT_FAILURE;
+ }
+
+ auto [globalOptions, consumed] = std::move(*opts);
+
+ if (consumed >= argc)
+ {
+ std::cerr << "Missing subcommand command type\n";
+ return EXIT_FAILURE;
+ }
+
+ /* We have parsed the global options, advance argv & argc to allow the
+ * subcommand handlers to consume their own options
+ */
+ argc -= consumed;
+ argv += consumed;
+
+ std::string subcommand = argv[0];
+ int ret = -1;
+
+ if (subcommand == "raw")
+ {
+ ret = ncsiCommandRaw(globalOptions, argc, argv);
+ }
+ else if (subcommand == "oem")
+ {
+ ret = ncsiCommandOEM(globalOptions, argc, argv);
+ }
+ else
+ {
+ std::cerr << "Unknown subcommand '" << subcommand << "'\n";
+ print_usage(progname);
+ }
+
+ return ret ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/src/ncsi_netlink_main.cpp b/src/ncsi_netlink_main.cpp
index 8859950..09010fb 100644
--- a/src/ncsi_netlink_main.cpp
+++ b/src/ncsi_netlink_main.cpp
@@ -16,6 +16,9 @@
#include "argument.hpp"
#include "ncsi_util.hpp"
+#include <string.h>
+#include <unistd.h>
+
#include <phosphor-logging/lg2.hpp>
#include <stdplus/numeric/str.hpp>
#include <stdplus/str/buf.hpp>
@@ -74,26 +77,6 @@
}
}
-static stdplus::StrBuf toHexStr(std::span<const uint8_t> c) noexcept
-{
- stdplus::StrBuf ret;
- if (c.empty())
- {
- return ret;
- }
- stdplus::IntToStr<16, uint8_t> its;
- auto oit = ret.append(c.size() * 3);
- auto cit = c.begin();
- oit = its(oit, *cit++, 2);
- for (; cit != c.end(); ++cit)
- {
- *oit++ = ' ';
- oit = its(oit, *cit, 2);
- }
- *oit = 0;
- return ret;
-}
-
int main(int argc, char** argv)
{
using namespace phosphor::network;
@@ -103,7 +86,6 @@
int packageInt{};
int channelInt{};
int indexInt{};
- int operationInt{DEFAULT_VALUE};
// Parse out interface argument.
auto ifIndex = (options)["index"];
@@ -159,69 +141,60 @@
auto payloadStr = (options)["oem-payload"];
if (!payloadStr.empty())
{
- std::string byte(2, '\0');
- std::vector<unsigned char> payload;
-
- if (payloadStr.size() % 2)
+ if (payloadStr.size() % 2 || payloadStr.size() < 2)
exitWithError("Payload invalid: specify two hex digits per byte.",
argv);
- // Parse the payload string (e.g. "50000001572100") to byte data
- // The first two characters (i.e. "50") represent the Send Cmd Operation
- // All remaining pairs, interpreted in hex radix, represent the command
- // payload
- int sendCmdSelect{};
- for (unsigned int i = 1; i < payloadStr.size(); i += 2)
- {
- byte[0] = payloadStr[i - 1];
- byte[1] = payloadStr[i];
-
- try
- {
- sendCmdSelect = stoi(byte, nullptr, 16);
- }
- catch (const std::exception& e)
- {
- exitWithError("Payload invalid.", argv);
- }
- if (i == 1)
- {
- operationInt = sendCmdSelect;
- }
- else
- {
- payload.push_back(sendCmdSelect);
- }
- }
-
- if (operationInt == DEFAULT_VALUE)
- {
- exitWithError("No payload specified.", argv);
- }
+ // Payload string is in the format <type>[<payload>]
+ // (e.g. "50000001572100"), where the first two characters (i.e. "50")
+ // represent the command type, and the rest the payload. Split this
+ // up for the ncsi-cmd operation, which has these as separate arguments.
+ std::string typeStr(payloadStr.substr(0, 2));
+ std::string dataStr(payloadStr.substr(2));
if (packageInt == DEFAULT_VALUE)
{
exitWithError("Package not specified.", argv);
}
- if (!payload.empty())
+ std::vector<std::string> args = {
+ "ncsi-cmd",
+ "-i",
+ std::to_string(indexInt),
+ "-p",
+ std::to_string(packageInt),
+ "raw",
+ };
+
+ if (channelInt != DEFAULT_VALUE)
{
- lg2::debug("Payload: {PAYLOAD}", "PAYLOAD", toHexStr(payload));
+ args.push_back("-c");
+ args.push_back(std::to_string(channelInt));
}
- std::optional<uint8_t> chan = channelInt != DEFAULT_VALUE
- ? std::make_optional(channelInt)
- : std::nullopt;
- NCSICommand cmd(operationInt, packageInt, chan, payload);
+ args.push_back(typeStr);
+ args.push_back(dataStr);
- auto resp = interface.sendCommand(cmd);
- if (!resp)
+ /* Convert to C argv array. execvp()'s argv argument is not const,
+ * whereas .c_str() is, so we need to strdup here.
+ */
+ char** argv = new char*[args.size() + 1]();
+ for (size_t i = 0; i < args.size(); i++)
{
- return EXIT_FAILURE;
+ argv[i] = strdup(args[i].c_str());
}
- lg2::debug("Response {DATA_LEN} bytes: {DATA}", "DATA_LEN",
- resp->full_payload.size(), "DATA",
- toHexStr(resp->full_payload));
+ argv[args.size()] = NULL;
+
+ lg2::debug("ncsi-netlink [..] -o is deprecated by ncsi-cmd");
+ execvp(argv[0], argv);
+ lg2::error("exec failed; use ncsi-cmd directly");
+
+ for (size_t i = 0; i < args.size(); i++)
+ {
+ free(argv[i]);
+ }
+ delete[] argv;
+ return EXIT_FAILURE;
}
else if ((options)["set"] == "true")
{