blob: ea0bc12fcd4cbe08f5f0428e50357ad6fb2403bd [file] [log] [blame]
Jeremy Kerrbad17c02024-11-21 16:44:39 +08001/**
2 * Copyright © 2018 IBM Corporation
3 * Copyright © 2024 Code Construct
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17#include "ncsi_util.hpp"
18
19#include <assert.h>
20#include <getopt.h>
Jeremy Kerrca9d8672024-09-16 14:22:02 +080021#include <linux/mctp.h>
Jeremy Kerrbad17c02024-11-21 16:44:39 +080022#include <stdint.h>
23#include <stdlib.h>
24#include <string.h>
25
26#include <phosphor-logging/lg2.hpp>
27#include <stdplus/numeric/str.hpp>
28#include <stdplus/str/buf.hpp>
29#include <stdplus/str/conv.hpp>
30
31#include <climits>
eddyluef2be3c2024-12-13 11:11:05 +080032#include <fstream>
Jeremy Kerrbad17c02024-11-21 16:44:39 +080033#include <iostream>
eddyluef2be3c2024-12-13 11:11:05 +080034#include <map>
Jeremy Kerrbad17c02024-11-21 16:44:39 +080035#include <memory>
36#include <optional>
37#include <string_view>
38#include <vector>
39
40using namespace phosphor::network::ncsi;
41
eddyluef2be3c2024-12-13 11:11:05 +080042const uint32_t NCSI_CORE_DUMP_HANDLE = 0xFFFF0000;
43const uint32_t NCSI_CRASH_DUMP_HANDLE = 0xFFFF0001;
44
Jeremy Kerrbad17c02024-11-21 16:44:39 +080045struct GlobalOptions
46{
47 std::unique_ptr<Interface> interface;
eddyluef2be3c2024-12-13 11:11:05 +080048 std::optional<unsigned int> package;
Jeremy Kerrbad17c02024-11-21 16:44:39 +080049 std::optional<unsigned int> channel;
50};
51
Jeremy Kerrca9d8672024-09-16 14:22:02 +080052struct MCTPAddress
53{
54 int network;
55 uint8_t eid;
56};
57
58/* MCTP EIDs below 8 are invalid, 255 is broadcast */
59static constexpr uint8_t MCTP_EID_MIN = 8;
60static constexpr uint8_t MCTP_EID_MAX = 254;
61
Jeremy Kerrbad17c02024-11-21 16:44:39 +080062const struct option options[] = {
63 {"package", required_argument, NULL, 'p'},
64 {"channel", required_argument, NULL, 'c'},
65 {"interface", required_argument, NULL, 'i'},
Jeremy Kerrca9d8672024-09-16 14:22:02 +080066 {"mctp", required_argument, NULL, 'm'},
Jeremy Kerrbad17c02024-11-21 16:44:39 +080067 {"help", no_argument, NULL, 'h'},
68 {0, 0, 0, 0},
69};
70
71static void print_usage(const char* progname)
72{
73 // clang-format off
74 std::cerr
75 << "Usage:\n"
76 " " << progname << " <options> raw TYPE [PAYLOAD...]\n"
77 " " << progname << " <options> oem [PAYLOAD...]\n"
eddyluef2be3c2024-12-13 11:11:05 +080078 " " << progname << " <options> core-dump FILE\n"
79 " " << progname << " <options> crash-dump FILE\n"
Jeremy Kerrbad17c02024-11-21 16:44:39 +080080 "\n"
81 "Global options:\n"
82 " --interface IFACE, -i Specify net device by ifindex.\n"
Jeremy Kerrca9d8672024-09-16 14:22:02 +080083 " --mctp [NET,]EID, -m Specify MCTP network device.\n"
eddyluef2be3c2024-12-13 11:11:05 +080084 " --package PACKAGE, -p For non-discovery commands this is required; for discovery it is optional and\n"
85 " restricts the discovery to a specific package index.\n"
Jeremy Kerrbad17c02024-11-21 16:44:39 +080086 " --channel CHANNEL, -c Specify a channel.\n"
87 "\n"
Jeremy Kerrca9d8672024-09-16 14:22:02 +080088 "A --package/-p argument is required, as well as interface type "
89 "(--interface/-i or --mctp/-m)\n"
Jeremy Kerrbad17c02024-11-21 16:44:39 +080090 "\n"
91 "Subcommands:\n"
92 "\n"
93 "raw TYPE [PAYLOAD...]\n"
94 " Send a single command using raw type/payload data.\n"
95 " TYPE NC-SI command type, in hex\n"
96 " PAYLOAD Command payload bytes, as hex\n"
97 "\n"
98 "oem PAYLOAD\n"
99 " Send a single OEM command (type 0x50).\n"
eddyluef2be3c2024-12-13 11:11:05 +0800100 " PAYLOAD Command payload bytes, as hex\n"
101 "\n"
102 "core-dump FILE\n"
103 " Perform NCSI core dump and save log to FILE.\n"
104 "\n"
105 "crash-dump FILE\n"
106 " Perform NCSI crash dump and save log to FILE.\n";
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800107 // clang-format on
108}
109
110static std::optional<unsigned int>
111 parseUnsigned(const char* str, const char* label)
112{
113 try
114 {
115 unsigned long tmp = std::stoul(str, NULL, 16);
116 if (tmp <= UINT_MAX)
117 return tmp;
118 }
119 catch (const std::exception& e)
120 {}
121 std::cerr << "Invalid " << label << " argument '" << str << "'\n";
122 return {};
123}
124
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800125static std::optional<MCTPAddress> parseMCTPAddress(const std::string& str)
126{
127 std::string::size_type sep = str.find(',');
128 std::string eid_str;
129 MCTPAddress addr;
130
131 if (sep == std::string::npos)
132 {
133 addr.network = MCTP_NET_ANY;
134 eid_str = str;
135 }
136 else
137 {
138 std::string net_str = str.substr(0, sep);
139 try
140 {
141 addr.network = stoi(net_str);
142 }
143 catch (const std::exception& e)
144 {
145 return {};
146 }
147 eid_str = str.substr(sep + 1);
148 }
149
150 unsigned long tmp;
151 try
152 {
153 tmp = stoul(eid_str);
154 }
155 catch (const std::exception& e)
156 {
157 return {};
158 }
159
160 if (tmp < MCTP_EID_MIN || tmp > MCTP_EID_MAX)
161 {
162 return {};
163 }
164
165 addr.eid = tmp;
166
167 return addr;
168}
169
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800170static std::optional<std::vector<unsigned char>>
171 parsePayload(int argc, const char* const argv[])
172{
173 /* we have already checked that there are sufficient args in callers */
174 assert(argc >= 1);
175
176 std::vector<unsigned char> payload;
177
178 /* we support two formats of payload - all as one argument:
179 * 00010c202f
180 *
181 * or single bytes in separate arguments:
182 * 00 01 0c 20 2f
183 *
184 * both are assumed as entirely hex, but the latter format does not
185 * need to be exactly two chars per byte:
186 * 0 1 c 20 2f
187 */
188
189 size_t len0 = strlen(argv[0]);
190 if (argc == 1 && len0 > 2)
191 {
192 /* single argument format, parse as multiple bytes */
193 if (len0 % 2 != 0)
194 {
195 std::cerr << "Invalid payload length " << len0
196 << " (must be a multiple of 2 chars)\n";
197 return {};
198 }
199
200 std::string str(argv[0]);
201 std::string_view sv(str);
202
203 for (unsigned int i = 0; i < sv.size(); i += 2)
204 {
205 unsigned char byte;
206 auto begin = sv.data() + i;
207 auto end = begin + 2;
208
209 auto [next, err] = std::from_chars(begin, end, byte, 16);
210
211 if (err != std::errc() || next != end)
212 {
213 std::cerr << "Invalid payload string\n";
214 return {};
215 }
216 payload.push_back(byte);
217 }
218 }
219 else
220 {
221 /* multiple payload arguments, each is a separate hex byte */
222 for (int i = 0; i < argc; i++)
223 {
224 unsigned char byte;
225 auto begin = argv[i];
226 auto end = begin + strlen(begin);
227
228 auto [next, err] = std::from_chars(begin, end, byte, 16);
229
230 if (err != std::errc() || next != end)
231 {
232 std::cerr << "Invalid payload argument '" << begin << "'\n";
233 return {};
234 }
235 payload.push_back(byte);
236 }
237 }
238
239 return payload;
240}
241
242static std::optional<std::tuple<GlobalOptions, int>>
243 parseGlobalOptions(int argc, char* const* argv)
244{
245 std::optional<unsigned int> chan, package, interface;
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800246 std::optional<MCTPAddress> mctp;
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800247 const char* progname = argv[0];
248 GlobalOptions opts{};
249
250 for (;;)
251 {
252 /* We're using + here as we want to stop parsing at the subcommand
253 * name
254 */
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800255 int opt = getopt_long(argc, argv, "+p:c:i:m:h", options, NULL);
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800256 if (opt == -1)
257 {
258 break;
259 }
260
261 switch (opt)
262 {
263 case 'i':
264 interface = parseUnsigned(optarg, "interface");
265 if (!interface.has_value())
266 {
267 return {};
268 }
269 break;
270
271 case 'p':
272 package = parseUnsigned(optarg, "package");
273 if (!package.has_value())
274 {
275 return {};
276 }
277 break;
278
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800279 case 'm':
280 mctp = parseMCTPAddress(optarg);
281 if (!mctp.has_value())
282 {
283 return {};
284 }
285 break;
286
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800287 case 'c':
288 chan = parseUnsigned(optarg, "channel");
289 if (!chan.has_value())
290 {
291 return {};
292 }
293 opts.channel = *chan;
294 break;
295
296 case 'h':
297 default:
298 print_usage(progname);
299 return {};
300 }
301 }
302
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800303 if (interface.has_value() && mctp.has_value())
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800304 {
Jeremy Kerrca9d8672024-09-16 14:22:02 +0800305 std::cerr << "Only one of --interface or --mctp can be provided\n";
306 return {};
307 }
308 else if (interface.has_value())
309 {
310 opts.interface = std::make_unique<NetlinkInterface>(*interface);
311 }
312 else if (mctp.has_value())
313 {
314 MCTPAddress m = *mctp;
315 opts.interface = std::make_unique<MCTPInterface>(m.network, m.eid);
316 }
317 else
318 {
319 std::cerr << "Missing interface description, "
320 "add a --mctp or --interface argument\n";
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800321 return {};
322 }
323
324 if (!package.has_value())
325 {
326 std::cerr << "Missing package, add a --package argument\n";
327 return {};
328 }
329
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800330 opts.package = *package;
331
332 return std::make_tuple(std::move(opts), optind);
333}
334
335static stdplus::StrBuf toHexStr(std::span<const uint8_t> c) noexcept
336{
337 stdplus::StrBuf ret;
338 if (c.empty())
339 {
340 /* workaround for lg2's handling of string_view */
341 *ret.data() = '\0';
342 return ret;
343 }
344 stdplus::IntToStr<16, uint8_t> its;
345 auto oit = ret.append(c.size() * 3);
346 auto cit = c.begin();
347 oit = its(oit, *cit++, 2);
348 for (; cit != c.end(); ++cit)
349 {
350 *oit++ = ' ';
351 oit = its(oit, *cit, 2);
352 }
353 *oit = 0;
354 return ret;
355}
356
357/* Helper for the 'raw' and 'oem' command handlers: Construct a single command,
358 * issue it to the interface, and print the resulting response payload.
359 */
360static int ncsiCommand(GlobalOptions& options, uint8_t type,
361 std::vector<unsigned char> payload)
362{
eddyluef2be3c2024-12-13 11:11:05 +0800363 NCSICommand cmd(type, static_cast<uint8_t>(*options.package),
364 options.channel,
365 std::span<unsigned char>(payload.data(), payload.size()));
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800366
367 lg2::debug("Command: type {TYPE}, payload {PAYLOAD_LEN} bytes: {PAYLOAD}",
368 "TYPE", lg2::hex, type, "PAYLOAD_LEN", payload.size(), "PAYLOAD",
369 toHexStr(payload));
370
371 auto resp = options.interface->sendCommand(cmd);
372 if (!resp)
373 {
374 return -1;
375 }
376
377 lg2::debug("Response {DATA_LEN} bytes: {DATA}", "DATA_LEN",
378 resp->full_payload.size(), "DATA", toHexStr(resp->full_payload));
379
380 return 0;
381}
382
383static int ncsiCommandRaw(GlobalOptions& options, int argc,
384 const char* const* argv)
385{
386 std::vector<unsigned char> payload;
387 std::optional<uint8_t> type;
388
389 if (argc < 2)
390 {
391 std::cerr << "Invalid arguments for 'raw' subcommand\n";
392 return -1;
393 }
394
395 /* Not only does the type need to fit into one byte, but the top bit
396 * is used for the request/response flag, so check for 0x80 here as
397 * our max here.
398 */
399 type = parseUnsigned(argv[1], "command type");
400 if (!type.has_value() || *type > 0x80)
401 {
402 std::cerr << "Invalid command type value\n";
403 return -1;
404 }
405
406 if (argc >= 3)
407 {
408 auto tmp = parsePayload(argc - 2, argv + 2);
409 if (!tmp.has_value())
410 {
411 return -1;
412 }
413
414 payload = *tmp;
415 }
416
417 return ncsiCommand(options, *type, payload);
418}
419
420static int ncsiCommandOEM(GlobalOptions& options, int argc,
421 const char* const* argv)
422{
423 constexpr uint8_t oemType = 0x50;
424
425 if (argc < 2)
426 {
427 std::cerr << "Invalid arguments for 'oem' subcommand\n";
428 return -1;
429 }
430
431 auto payload = parsePayload(argc - 1, argv + 1);
432 if (!payload.has_value())
433 {
434 return -1;
435 }
436
437 return ncsiCommand(options, oemType, *payload);
438}
439
eddyluef2be3c2024-12-13 11:11:05 +0800440static std::array<unsigned char, 12>
441 generateDumpCmdPayload(uint32_t chunkNum, uint32_t dataHandle, bool isAbort)
442{
443 std::array<unsigned char, 12> payload = {};
444 uint8_t opcode;
445
446 if (isAbort)
447 {
448 opcode = 3;
449 }
450 else if (chunkNum == 1)
451 {
452 // For the first chunk the chunk number field carries the data handle.
453 opcode = 0;
454 chunkNum = dataHandle;
455 }
456 else
457 {
458 opcode = 2;
459 }
460 payload[3] = opcode;
461 payload[8] = (chunkNum >> 24) & 0xFF;
462 payload[9] = (chunkNum >> 16) & 0xFF;
463 payload[10] = (chunkNum >> 8) & 0xFF;
464 payload[11] = chunkNum & 0xFF;
465
466 return payload;
467}
468
469std::string getDescForResponse(uint16_t response)
470{
471 static const std::map<uint16_t, std::string> descMap = {
472 {0x0000, "Command Completed"},
473 {0x0001, "Command Failed"},
474 {0x0002, "Command Unavailable"},
475 {0x0003, "Command Unsupported"},
476 {0x0004, "Delayed Response"}};
477
478 try
479 {
480 return descMap.at(response);
481 }
482 catch (std::exception&)
483 {
484 return "Unknown response code: " + std::to_string(response);
485 }
486}
487
488std::string getDescForReason(uint16_t reason)
489{
490 static const std::map<uint16_t, std::string> reasonMap = {
491 {0x0001, "Interface Initialization Required"},
492 {0x0002, "Parameter Is Invalid, Unsupported, or Out-of-Range"},
493 {0x0003, "Channel Not Ready"},
494 {0x0004, "Package Not Ready"},
495 {0x0005, "Invalid Payload Length"},
496 {0x0006, "Information Not Available"},
497 {0x0007, "Intervention Required"},
498 {0x0008, "Link Command Failed - Hardware Access Error"},
499 {0x0009, "Command Timeout"},
500 {0x000A, "Secondary Device Not Powered"},
501 {0x7FFF, "Unknown/Unsupported Command Type"},
502 {0x4D01, "Abort Transfer: NC cannot proceed with transfer."},
503 {0x4D02,
504 "Invalid Handle Value: Data Handle is invalid or not supported."},
505 {0x4D03,
506 "Sequence Count Error: Chunk Number requested is not consecutive with the previous number transmitted."}};
507
508 if (reason >= 0x8000)
509 {
510 return "OEM Reason Code" + std::to_string(reason);
511 }
512
513 try
514 {
515 return reasonMap.at(reason);
516 }
517 catch (std::exception&)
518 {
519 return "Unknown reason code: " + std::to_string(reason);
520 }
521}
522
523static int ncsiDump(GlobalOptions& options, uint32_t handle,
524 const std::string& fileName)
525{
526 constexpr auto ncsiCmdDump = 0x4D;
527 uint32_t chunkNum = 1;
528 bool isTransferComplete = false;
529 bool isAbort = false;
530 uint8_t opcode = 0;
531 uint32_t totalDataSize = 0;
532 std::ofstream outFile(fileName, std::ios::binary);
533
534 // Validate handle
535 if (handle != NCSI_CORE_DUMP_HANDLE && handle != NCSI_CRASH_DUMP_HANDLE)
536 {
537 std::cerr
538 << "Invalid data handle value. Expected NCSI_CORE_DUMP_HANDLE (0xFFFF0000) or NCSI_CRASH_DUMP_HANDLE (0xFFFF0001), got: "
539 << std::hex << handle << "\n";
540 if (outFile.is_open())
541 outFile.close();
542 return -1;
543 }
544
545 if (!outFile.is_open())
546 {
547 std::cerr << "Failed to open file: " << fileName << "\n";
548 return -1;
549 }
550
551 while (!isTransferComplete && !isAbort)
552 {
553 auto payloadArray = generateDumpCmdPayload(chunkNum, handle, false);
554 std::span<unsigned char> payload(payloadArray.data(),
555 payloadArray.size());
556
557 NCSICommand cmd(ncsiCmdDump, static_cast<uint8_t>(*options.package),
558 options.channel, payload);
559 auto resp = options.interface->sendCommand(cmd);
560 if (!resp)
561 {
562 std::cerr << "Failed to send NCSI command for chunk number "
563 << chunkNum << "\n";
564 outFile.close();
565 return -1;
566 }
567
568 auto response = resp->response;
569 auto reason = resp->reason;
570 auto length = resp->payload.size();
571
572 if (response != 0)
573 {
574 std::cerr << "Error encountered on chunk " << chunkNum << ":\n"
575 << "Response Description: "
576 << getDescForResponse(response) << "\n"
577 << "Reason Description: " << getDescForReason(reason)
578 << "\n";
579 outFile.close();
580 return -1;
581 }
582
583 if (length > 8)
584 {
585 auto dataSize = length - 8;
586 totalDataSize += dataSize;
587 opcode = resp->payload[7];
588 if (outFile.is_open())
589 {
590 outFile.write(
591 reinterpret_cast<const char*>(resp->payload.data() + 8),
592 dataSize);
593 }
594 else
595 {
596 std::cerr << "Failed to write to file. File is not open.\n";
597 isAbort = true;
598 }
599 }
600 else
601 {
602 std::cerr << "Received response with insufficient payload length: "
603 << length << " Expected more than 8 bytes. Chunk: "
604 << chunkNum << "\n";
605 isAbort = true;
606 }
607
608 switch (opcode)
609 {
610 case 0x1: // Initial chunk, continue to next
611 case 0x2: // Middle chunk, continue to next
612 chunkNum++;
613 break;
614 case 0x4: // Final chunk
615 case 0x5: // Initial and final chunk
616 isTransferComplete = true;
617 break;
618 case 0x8: // Abort transfer
619 std::cerr << "Transfer aborted by NIC\n";
620 isTransferComplete = true;
621 break;
622 default:
623 std::cerr << "Unexpected opcode: " << static_cast<int>(opcode)
624 << " at chunk " << chunkNum << "\n";
625 isAbort = true;
626 break;
627 }
628 }
629
630 // Handle abort explicitly if an unexpected opcode was encountered.
631 if (isAbort)
632 {
633 std::cerr << "Issuing explicit abort command...\n";
634 auto abortPayloadArray = generateDumpCmdPayload(chunkNum, handle, true);
635 std::span<unsigned char> abortPayload(abortPayloadArray.data(),
636 abortPayloadArray.size());
637 NCSICommand abortCmd(ncsiCmdDump,
638 static_cast<uint8_t>(*options.package),
639 options.channel, abortPayload);
640 auto abortResp = options.interface->sendCommand(abortCmd);
641 if (!abortResp)
642 {
643 std::cerr << "Failed to send abort command for chunk number "
644 << chunkNum << "\n";
645 }
646 else
647 {
648 std::cerr << "Abort command issued.\n";
649 }
650 }
651 else
652 {
653 std::cout << "Dump transfer complete. Total data size: "
654 << totalDataSize << " bytes\n";
655 }
656
657 outFile.close();
658 return 0;
659}
660
661static int ncsiCommandReceiveDump(GlobalOptions& options,
662 const std::string& subcommand, int argc,
663 const char* const* argv)
664{
665 if (argc != 2)
666 {
667 std::cerr << "Invalid arguments for '" << subcommand
668 << "' subcommand\n";
669 print_usage(argv[0]);
670 return -1;
671 }
672 uint32_t handle = (subcommand == "core-dump") ? NCSI_CORE_DUMP_HANDLE
673 : NCSI_CRASH_DUMP_HANDLE;
674 return ncsiDump(options, handle, argv[1]);
675}
676
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800677/* A note on log output:
678 * For output that relates to command-line usage, we just output directly to
679 * stderr. Once we have a properly parsed command line invocation, we use lg2
680 * for log output, as we want that to use the standard log facilities to
681 * catch runtime error scenarios
682 */
683int main(int argc, char** argv)
684{
685 const char* progname = argv[0];
686
687 auto opts = parseGlobalOptions(argc, argv);
688
689 if (!opts.has_value())
690 {
691 return EXIT_FAILURE;
692 }
693
694 auto [globalOptions, consumed] = std::move(*opts);
695
696 if (consumed >= argc)
697 {
698 std::cerr << "Missing subcommand command type\n";
699 return EXIT_FAILURE;
700 }
701
702 /* We have parsed the global options, advance argv & argc to allow the
703 * subcommand handlers to consume their own options
704 */
705 argc -= consumed;
706 argv += consumed;
707
708 std::string subcommand = argv[0];
709 int ret = -1;
710
711 if (subcommand == "raw")
712 {
713 ret = ncsiCommandRaw(globalOptions, argc, argv);
714 }
715 else if (subcommand == "oem")
716 {
717 ret = ncsiCommandOEM(globalOptions, argc, argv);
718 }
eddyluef2be3c2024-12-13 11:11:05 +0800719 else if (subcommand == "core-dump" || subcommand == "crash-dump")
720 {
721 ret = ncsiCommandReceiveDump(globalOptions, subcommand, argc, argv);
722 }
Jeremy Kerrbad17c02024-11-21 16:44:39 +0800723 else
724 {
725 std::cerr << "Unknown subcommand '" << subcommand << "'\n";
726 print_usage(progname);
727 }
728
729 return ret ? EXIT_FAILURE : EXIT_SUCCESS;
730}