/** * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace phosphor::network::ncsi; const uint32_t NCSI_CORE_DUMP_HANDLE = 0xFFFF0000; const uint32_t NCSI_CRASH_DUMP_HANDLE = 0xFFFF0001; struct GlobalOptions { std::unique_ptr interface; std::optional package; std::optional channel; }; struct MCTPAddress { int network; uint8_t eid; }; /* MCTP EIDs below 8 are invalid, 255 is broadcast */ static constexpr uint8_t MCTP_EID_MIN = 8; static constexpr uint8_t MCTP_EID_MAX = 254; const struct option options[] = { {"package", required_argument, NULL, 'p'}, {"channel", required_argument, NULL, 'c'}, {"interface", required_argument, NULL, 'i'}, {"mctp", required_argument, NULL, 'm'}, {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0}, }; static void print_usage(const char* progname) { // clang-format off std::cerr << "Usage:\n" " " << progname << " raw TYPE [PAYLOAD...]\n" " " << progname << " oem [PAYLOAD...]\n" " " << progname << " discover\n" " " << progname << " core-dump FILE\n" " " << progname << " crash-dump FILE\n" "\n" "Global options:\n" " --interface IFACE, -i Specify net device by ifindex.\n" " --mctp [NET,]EID, -m Specify MCTP network device.\n" " --package PACKAGE, -p For non-discovery commands this is required; for discovery it is optional and\n" " restricts the discovery to a specific package index.\n" " --channel CHANNEL, -c Specify a channel.\n" "\n" "A --package/-p argument is required, as well as interface type " "(--interface/-i or --mctp/-m)\n" "\n" "Subcommands:\n" "\n" "discover\n" " Scan for available NC-SI packages and channels.\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" "\n" "core-dump FILE\n" " Perform NCSI core dump and save log to FILE.\n" "\n" "crash-dump FILE\n" " Perform NCSI crash dump and save log to FILE.\n"; // clang-format on } static std::optional 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 parseMCTPAddress(const std::string& str) { std::string::size_type sep = str.find(','); std::string eid_str; MCTPAddress addr; if (sep == std::string::npos) { addr.network = MCTP_NET_ANY; eid_str = str; } else { std::string net_str = str.substr(0, sep); try { addr.network = stoi(net_str); } catch (const std::exception& e) { return {}; } eid_str = str.substr(sep + 1); } unsigned long tmp; try { tmp = stoul(eid_str); } catch (const std::exception& e) { return {}; } if (tmp < MCTP_EID_MIN || tmp > MCTP_EID_MAX) { return {}; } addr.eid = tmp; return addr; } static std::optional> parsePayload( int argc, const char* const argv[]) { /* we have already checked that there are sufficient args in callers */ assert(argc >= 1); std::vector 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> parseGlobalOptions( int argc, char* const* argv) { std::optional chan, package, interface; std::optional mctp; 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:m: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 'm': mctp = parseMCTPAddress(optarg); if (!mctp.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() && mctp.has_value()) { std::cerr << "Only one of --interface or --mctp can be provided\n"; return {}; } else if (interface.has_value()) { opts.interface = std::make_unique(*interface); } else if (mctp.has_value()) { MCTPAddress m = *mctp; opts.interface = std::make_unique(m.network, m.eid); } else { std::cerr << "Missing interface description, " "add a --mctp or --interface argument\n"; return {}; } // For non-discovery commands, package is required. // If the subcommand is "discover", leave opts.package as nullopt. if (!package.has_value()) { if (optind < argc && std::string(argv[optind]) == "discover") { // Do nothing; package remains nullopt for discovery. } else { std::cerr << "Missing package, add a --package argument\n"; return {}; } } else { opts.package = static_cast(*package); } return std::make_tuple(std::move(opts), optind); } static stdplus::StrBuf toHexStr(std::span 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 payload) { uint8_t pkg = options.package.value_or(0); auto ch = options.channel; NCSICommand cmd(type, pkg, ch, std::span(payload.data(), payload.size())); 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 payload; std::optional 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); } static std::array generateDumpCmdPayload( uint32_t chunkNum, uint32_t dataHandle, bool isAbort) { std::array payload = {}; uint8_t opcode; if (isAbort) { opcode = 3; } else if (chunkNum == 1) { // For the first chunk the chunk number field carries the data handle. opcode = 0; chunkNum = dataHandle; } else { opcode = 2; } payload[3] = opcode; payload[8] = (chunkNum >> 24) & 0xFF; payload[9] = (chunkNum >> 16) & 0xFF; payload[10] = (chunkNum >> 8) & 0xFF; payload[11] = chunkNum & 0xFF; return payload; } std::string getDescForResponse(uint16_t response) { static const std::map descMap = { {0x0000, "Command Completed"}, {0x0001, "Command Failed"}, {0x0002, "Command Unavailable"}, {0x0003, "Command Unsupported"}, {0x0004, "Delayed Response"}}; try { return descMap.at(response); } catch (std::exception&) { return "Unknown response code: " + std::to_string(response); } } std::string getDescForReason(uint16_t reason) { static const std::map reasonMap = { {0x0001, "Interface Initialization Required"}, {0x0002, "Parameter Is Invalid, Unsupported, or Out-of-Range"}, {0x0003, "Channel Not Ready"}, {0x0004, "Package Not Ready"}, {0x0005, "Invalid Payload Length"}, {0x0006, "Information Not Available"}, {0x0007, "Intervention Required"}, {0x0008, "Link Command Failed - Hardware Access Error"}, {0x0009, "Command Timeout"}, {0x000A, "Secondary Device Not Powered"}, {0x7FFF, "Unknown/Unsupported Command Type"}, {0x4D01, "Abort Transfer: NC cannot proceed with transfer."}, {0x4D02, "Invalid Handle Value: Data Handle is invalid or not supported."}, {0x4D03, "Sequence Count Error: Chunk Number requested is not consecutive with the previous number transmitted."}}; if (reason >= 0x8000) { return "OEM Reason Code" + std::to_string(reason); } try { return reasonMap.at(reason); } catch (std::exception&) { return "Unknown reason code: " + std::to_string(reason); } } static int ncsiDump(GlobalOptions& options, uint32_t handle, const std::string& fileName) { constexpr auto ncsiCmdDump = 0x4D; uint32_t chunkNum = 1; bool isTransferComplete = false; bool isAbort = false; uint8_t opcode = 0; uint32_t totalDataSize = 0; std::ofstream outFile(fileName, std::ios::binary); // Validate handle if (handle != NCSI_CORE_DUMP_HANDLE && handle != NCSI_CRASH_DUMP_HANDLE) { std::cerr << "Invalid data handle value. Expected NCSI_CORE_DUMP_HANDLE (0xFFFF0000) or NCSI_CRASH_DUMP_HANDLE (0xFFFF0001), got: " << std::hex << handle << "\n"; if (outFile.is_open()) outFile.close(); return -1; } if (!outFile.is_open()) { std::cerr << "Failed to open file: " << fileName << "\n"; return -1; } while (!isTransferComplete && !isAbort) { auto payloadArray = generateDumpCmdPayload(chunkNum, handle, false); std::span payload(payloadArray.data(), payloadArray.size()); uint8_t pkg = options.package.value_or(0); auto ch = options.channel; NCSICommand cmd(ncsiCmdDump, pkg, ch, payload); auto resp = options.interface->sendCommand(cmd); if (!resp) { std::cerr << "Failed to send NCSI command for chunk number " << chunkNum << "\n"; outFile.close(); return -1; } auto response = resp->response; auto reason = resp->reason; auto length = resp->payload.size(); if (response != 0) { std::cerr << "Error encountered on chunk " << chunkNum << ":\n" << "Response Description: " << getDescForResponse(response) << "\n" << "Reason Description: " << getDescForReason(reason) << "\n"; outFile.close(); return -1; } if (length > 8) { auto dataSize = length - 8; totalDataSize += dataSize; opcode = resp->payload[7]; if (outFile.is_open()) { outFile.write( reinterpret_cast(resp->payload.data() + 8), dataSize); } else { std::cerr << "Failed to write to file. File is not open.\n"; isAbort = true; } } else { std::cerr << "Received response with insufficient payload length: " << length << " Expected more than 8 bytes. Chunk: " << chunkNum << "\n"; isAbort = true; } switch (opcode) { case 0x1: // Initial chunk, continue to next case 0x2: // Middle chunk, continue to next chunkNum++; break; case 0x4: // Final chunk case 0x5: // Initial and final chunk isTransferComplete = true; break; case 0x8: // Abort transfer std::cerr << "Transfer aborted by NIC\n"; isTransferComplete = true; break; default: std::cerr << "Unexpected opcode: " << static_cast(opcode) << " at chunk " << chunkNum << "\n"; isAbort = true; break; } } // Handle abort explicitly if an unexpected opcode was encountered. if (isAbort) { std::cerr << "Issuing explicit abort command...\n"; auto abortPayloadArray = generateDumpCmdPayload(chunkNum, handle, true); std::span abortPayload(abortPayloadArray.data(), abortPayloadArray.size()); uint8_t pkg = options.package.value_or(0); auto ch = options.channel; NCSICommand abortCmd(ncsiCmdDump, pkg, ch, abortPayload); auto abortResp = options.interface->sendCommand(abortCmd); if (!abortResp) { std::cerr << "Failed to send abort command for chunk number " << chunkNum << "\n"; } else { std::cerr << "Abort command issued.\n"; } } else { std::cout << "Dump transfer complete. Total data size: " << totalDataSize << " bytes\n"; } outFile.close(); return 0; } static int ncsiCommandReceiveDump(GlobalOptions& options, const std::string& subcommand, int argc, const char* const* argv) { if (argc != 2) { std::cerr << "Invalid arguments for '" << subcommand << "' subcommand\n"; print_usage(argv[0]); return -1; } uint32_t handle = (subcommand == "core-dump") ? NCSI_CORE_DUMP_HANDLE : NCSI_CRASH_DUMP_HANDLE; return ncsiDump(options, handle, argv[1]); } static int ncsiDiscover(GlobalOptions& options) { constexpr uint8_t ncsiClearInitialState = 0x00; constexpr uint8_t ncsiGetCapabilities = 0x16; constexpr unsigned int maxPackageIndex = 8; // Packages 0–7 constexpr unsigned int maxChannelCount = 32; // Channels 0–31 std::cout << "Starting NC-SI Package and Channel Discovery...\n"; unsigned int startPackage = 0, endPackage = maxPackageIndex; if (options.package.has_value()) { startPackage = *options.package; endPackage = *options.package; std::cout << "Restricting discovery to Package " << *options.package << ".\n"; } for (unsigned int packageIndex = startPackage; packageIndex <= endPackage; ++packageIndex) { std::cout << "Checking Package " << packageIndex << "...\n"; // For each channel from 0..7, we: // 1) Send Clear Initial State. // 2) If that succeeds, send Get Capabilities. // 3) If we get a valid response, we parse and stop. bool foundChannel = false; for (unsigned int channelIndex = 0; channelIndex < maxChannelCount; ++channelIndex) { std::cout << " Clearing Initial State on Channel " << channelIndex << "...\n"; { std::vector clearPayload; // No payload NCSICommand clearCmd(ncsiClearInitialState, static_cast(packageIndex), static_cast(channelIndex), std::span(clearPayload)); auto clearResp = options.interface->sendCommand(clearCmd); if (!clearResp || clearResp->response != 0x0000) { std::cout << " Clear Initial State failed on Channel " << channelIndex << ". Trying next channel.\n"; continue; // Try next channel } } // Now that Clear Initial State succeeded, try Get Capabilities std::vector payload; // No payload NCSICommand getCapCmd(ncsiGetCapabilities, static_cast(packageIndex), static_cast(channelIndex), std::span(payload)); auto resp = options.interface->sendCommand(getCapCmd); if (resp && resp->response == 0x0000 && resp->full_payload.size() >= 52) { uint8_t channelCount = resp->full_payload[47]; std::cout << " Package " << packageIndex << " supports " << static_cast(channelCount) << " channels.\n"; std::cout << " Found available Package " << packageIndex << ", Channel " << channelIndex << ". Stopping discovery.\n"; foundChannel = true; break; } else { std::cout << " Channel " << channelIndex << " not responding. Trying next channel.\n"; } } // end channel loop if (foundChannel) { // We found a channel on this package, so we stop discovery return 0; } else { std::cout << " No valid channels found for Package " << packageIndex << ". Moving to next package.\n"; } } // end package loop std::cout << "No available NC-SI packages or channels found. Discovery complete.\n"; return -1; } /* 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]; // For non-discovery commands, package must be provided. if (subcommand != "discover" && !globalOptions.package.has_value()) { std::cerr << "Missing package, add a --package argument\n"; return EXIT_FAILURE; } int ret = -1; if (subcommand == "raw") { ret = ncsiCommandRaw(globalOptions, argc, argv); } else if (subcommand == "oem") { ret = ncsiCommandOEM(globalOptions, argc, argv); } else if (subcommand == "core-dump" || subcommand == "crash-dump") { ret = ncsiCommandReceiveDump(globalOptions, subcommand, argc, argv); } else if (subcommand == "discover") { return ncsiDiscover(globalOptions); } else { std::cerr << "Unknown subcommand '" << subcommand << "'\n"; print_usage(progname); } return ret ? EXIT_FAILURE : EXIT_SUCCESS; }