1 /**
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>
21 #include <linux/mctp.h>
22 #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>
32 #include <iostream>
33 #include <memory>
34 #include <optional>
35 #include <string_view>
36 #include <vector>
37
38 using namespace phosphor::network::ncsi;
39
40 struct GlobalOptions
41 {
42 std::unique_ptr<Interface> interface;
43 unsigned int package;
44 std::optional<unsigned int> channel;
45 };
46
47 struct MCTPAddress
48 {
49 int network;
50 uint8_t eid;
51 };
52
53 /* MCTP EIDs below 8 are invalid, 255 is broadcast */
54 static constexpr uint8_t MCTP_EID_MIN = 8;
55 static constexpr uint8_t MCTP_EID_MAX = 254;
56
57 const struct option options[] = {
58 {"package", required_argument, NULL, 'p'},
59 {"channel", required_argument, NULL, 'c'},
60 {"interface", required_argument, NULL, 'i'},
61 {"mctp", required_argument, NULL, 'm'},
62 {"help", no_argument, NULL, 'h'},
63 {0, 0, 0, 0},
64 };
65
print_usage(const char * progname)66 static void print_usage(const char* progname)
67 {
68 // clang-format off
69 std::cerr
70 << "Usage:\n"
71 " " << progname << " <options> raw TYPE [PAYLOAD...]\n"
72 " " << progname << " <options> oem [PAYLOAD...]\n"
73 "\n"
74 "Global options:\n"
75 " --interface IFACE, -i Specify net device by ifindex.\n"
76 " --mctp [NET,]EID, -m Specify MCTP network device.\n"
77 " --package PACKAGE, -p Specify a package.\n"
78 " --channel CHANNEL, -c Specify a channel.\n"
79 "\n"
80 "A --package/-p argument is required, as well as interface type "
81 "(--interface/-i or --mctp/-m)\n"
82 "\n"
83 "Subcommands:\n"
84 "\n"
85 "raw TYPE [PAYLOAD...]\n"
86 " Send a single command using raw type/payload data.\n"
87 " TYPE NC-SI command type, in hex\n"
88 " PAYLOAD Command payload bytes, as hex\n"
89 "\n"
90 "oem PAYLOAD\n"
91 " Send a single OEM command (type 0x50).\n"
92 " PAYLOAD Command payload bytes, as hex\n";
93 // clang-format on
94 }
95
96 static std::optional<unsigned int>
parseUnsigned(const char * str,const char * label)97 parseUnsigned(const char* str, const char* label)
98 {
99 try
100 {
101 unsigned long tmp = std::stoul(str, NULL, 16);
102 if (tmp <= UINT_MAX)
103 return tmp;
104 }
105 catch (const std::exception& e)
106 {}
107 std::cerr << "Invalid " << label << " argument '" << str << "'\n";
108 return {};
109 }
110
parseMCTPAddress(const std::string & str)111 static std::optional<MCTPAddress> parseMCTPAddress(const std::string& str)
112 {
113 std::string::size_type sep = str.find(',');
114 std::string eid_str;
115 MCTPAddress addr;
116
117 if (sep == std::string::npos)
118 {
119 addr.network = MCTP_NET_ANY;
120 eid_str = str;
121 }
122 else
123 {
124 std::string net_str = str.substr(0, sep);
125 try
126 {
127 addr.network = stoi(net_str);
128 }
129 catch (const std::exception& e)
130 {
131 return {};
132 }
133 eid_str = str.substr(sep + 1);
134 }
135
136 unsigned long tmp;
137 try
138 {
139 tmp = stoul(eid_str);
140 }
141 catch (const std::exception& e)
142 {
143 return {};
144 }
145
146 if (tmp < MCTP_EID_MIN || tmp > MCTP_EID_MAX)
147 {
148 return {};
149 }
150
151 addr.eid = tmp;
152
153 return addr;
154 }
155
156 static std::optional<std::vector<unsigned char>>
parsePayload(int argc,const char * const argv[])157 parsePayload(int argc, const char* const argv[])
158 {
159 /* we have already checked that there are sufficient args in callers */
160 assert(argc >= 1);
161
162 std::vector<unsigned char> payload;
163
164 /* we support two formats of payload - all as one argument:
165 * 00010c202f
166 *
167 * or single bytes in separate arguments:
168 * 00 01 0c 20 2f
169 *
170 * both are assumed as entirely hex, but the latter format does not
171 * need to be exactly two chars per byte:
172 * 0 1 c 20 2f
173 */
174
175 size_t len0 = strlen(argv[0]);
176 if (argc == 1 && len0 > 2)
177 {
178 /* single argument format, parse as multiple bytes */
179 if (len0 % 2 != 0)
180 {
181 std::cerr << "Invalid payload length " << len0
182 << " (must be a multiple of 2 chars)\n";
183 return {};
184 }
185
186 std::string str(argv[0]);
187 std::string_view sv(str);
188
189 for (unsigned int i = 0; i < sv.size(); i += 2)
190 {
191 unsigned char byte;
192 auto begin = sv.data() + i;
193 auto end = begin + 2;
194
195 auto [next, err] = std::from_chars(begin, end, byte, 16);
196
197 if (err != std::errc() || next != end)
198 {
199 std::cerr << "Invalid payload string\n";
200 return {};
201 }
202 payload.push_back(byte);
203 }
204 }
205 else
206 {
207 /* multiple payload arguments, each is a separate hex byte */
208 for (int i = 0; i < argc; i++)
209 {
210 unsigned char byte;
211 auto begin = argv[i];
212 auto end = begin + strlen(begin);
213
214 auto [next, err] = std::from_chars(begin, end, byte, 16);
215
216 if (err != std::errc() || next != end)
217 {
218 std::cerr << "Invalid payload argument '" << begin << "'\n";
219 return {};
220 }
221 payload.push_back(byte);
222 }
223 }
224
225 return payload;
226 }
227
228 static std::optional<std::tuple<GlobalOptions, int>>
parseGlobalOptions(int argc,char * const * argv)229 parseGlobalOptions(int argc, char* const* argv)
230 {
231 std::optional<unsigned int> chan, package, interface;
232 std::optional<MCTPAddress> mctp;
233 const char* progname = argv[0];
234 GlobalOptions opts{};
235
236 for (;;)
237 {
238 /* We're using + here as we want to stop parsing at the subcommand
239 * name
240 */
241 int opt = getopt_long(argc, argv, "+p:c:i:m:h", options, NULL);
242 if (opt == -1)
243 {
244 break;
245 }
246
247 switch (opt)
248 {
249 case 'i':
250 interface = parseUnsigned(optarg, "interface");
251 if (!interface.has_value())
252 {
253 return {};
254 }
255 break;
256
257 case 'p':
258 package = parseUnsigned(optarg, "package");
259 if (!package.has_value())
260 {
261 return {};
262 }
263 break;
264
265 case 'm':
266 mctp = parseMCTPAddress(optarg);
267 if (!mctp.has_value())
268 {
269 return {};
270 }
271 break;
272
273 case 'c':
274 chan = parseUnsigned(optarg, "channel");
275 if (!chan.has_value())
276 {
277 return {};
278 }
279 opts.channel = *chan;
280 break;
281
282 case 'h':
283 default:
284 print_usage(progname);
285 return {};
286 }
287 }
288
289 if (interface.has_value() && mctp.has_value())
290 {
291 std::cerr << "Only one of --interface or --mctp can be provided\n";
292 return {};
293 }
294 else if (interface.has_value())
295 {
296 opts.interface = std::make_unique<NetlinkInterface>(*interface);
297 }
298 else if (mctp.has_value())
299 {
300 MCTPAddress m = *mctp;
301 opts.interface = std::make_unique<MCTPInterface>(m.network, m.eid);
302 }
303 else
304 {
305 std::cerr << "Missing interface description, "
306 "add a --mctp or --interface argument\n";
307 return {};
308 }
309
310 if (!package.has_value())
311 {
312 std::cerr << "Missing package, add a --package argument\n";
313 return {};
314 }
315
316 opts.package = *package;
317
318 return std::make_tuple(std::move(opts), optind);
319 }
320
toHexStr(std::span<const uint8_t> c)321 static stdplus::StrBuf toHexStr(std::span<const uint8_t> c) noexcept
322 {
323 stdplus::StrBuf ret;
324 if (c.empty())
325 {
326 /* workaround for lg2's handling of string_view */
327 *ret.data() = '\0';
328 return ret;
329 }
330 stdplus::IntToStr<16, uint8_t> its;
331 auto oit = ret.append(c.size() * 3);
332 auto cit = c.begin();
333 oit = its(oit, *cit++, 2);
334 for (; cit != c.end(); ++cit)
335 {
336 *oit++ = ' ';
337 oit = its(oit, *cit, 2);
338 }
339 *oit = 0;
340 return ret;
341 }
342
343 /* Helper for the 'raw' and 'oem' command handlers: Construct a single command,
344 * issue it to the interface, and print the resulting response payload.
345 */
ncsiCommand(GlobalOptions & options,uint8_t type,std::vector<unsigned char> payload)346 static int ncsiCommand(GlobalOptions& options, uint8_t type,
347 std::vector<unsigned char> payload)
348 {
349 NCSICommand cmd(type, options.package, options.channel, payload);
350
351 lg2::debug("Command: type {TYPE}, payload {PAYLOAD_LEN} bytes: {PAYLOAD}",
352 "TYPE", lg2::hex, type, "PAYLOAD_LEN", payload.size(), "PAYLOAD",
353 toHexStr(payload));
354
355 auto resp = options.interface->sendCommand(cmd);
356 if (!resp)
357 {
358 return -1;
359 }
360
361 lg2::debug("Response {DATA_LEN} bytes: {DATA}", "DATA_LEN",
362 resp->full_payload.size(), "DATA", toHexStr(resp->full_payload));
363
364 return 0;
365 }
366
ncsiCommandRaw(GlobalOptions & options,int argc,const char * const * argv)367 static int ncsiCommandRaw(GlobalOptions& options, int argc,
368 const char* const* argv)
369 {
370 std::vector<unsigned char> payload;
371 std::optional<uint8_t> type;
372
373 if (argc < 2)
374 {
375 std::cerr << "Invalid arguments for 'raw' subcommand\n";
376 return -1;
377 }
378
379 /* Not only does the type need to fit into one byte, but the top bit
380 * is used for the request/response flag, so check for 0x80 here as
381 * our max here.
382 */
383 type = parseUnsigned(argv[1], "command type");
384 if (!type.has_value() || *type > 0x80)
385 {
386 std::cerr << "Invalid command type value\n";
387 return -1;
388 }
389
390 if (argc >= 3)
391 {
392 auto tmp = parsePayload(argc - 2, argv + 2);
393 if (!tmp.has_value())
394 {
395 return -1;
396 }
397
398 payload = *tmp;
399 }
400
401 return ncsiCommand(options, *type, payload);
402 }
403
ncsiCommandOEM(GlobalOptions & options,int argc,const char * const * argv)404 static int ncsiCommandOEM(GlobalOptions& options, int argc,
405 const char* const* argv)
406 {
407 constexpr uint8_t oemType = 0x50;
408
409 if (argc < 2)
410 {
411 std::cerr << "Invalid arguments for 'oem' subcommand\n";
412 return -1;
413 }
414
415 auto payload = parsePayload(argc - 1, argv + 1);
416 if (!payload.has_value())
417 {
418 return -1;
419 }
420
421 return ncsiCommand(options, oemType, *payload);
422 }
423
424 /* A note on log output:
425 * For output that relates to command-line usage, we just output directly to
426 * stderr. Once we have a properly parsed command line invocation, we use lg2
427 * for log output, as we want that to use the standard log facilities to
428 * catch runtime error scenarios
429 */
main(int argc,char ** argv)430 int main(int argc, char** argv)
431 {
432 const char* progname = argv[0];
433
434 auto opts = parseGlobalOptions(argc, argv);
435
436 if (!opts.has_value())
437 {
438 return EXIT_FAILURE;
439 }
440
441 auto [globalOptions, consumed] = std::move(*opts);
442
443 if (consumed >= argc)
444 {
445 std::cerr << "Missing subcommand command type\n";
446 return EXIT_FAILURE;
447 }
448
449 /* We have parsed the global options, advance argv & argc to allow the
450 * subcommand handlers to consume their own options
451 */
452 argc -= consumed;
453 argv += consumed;
454
455 std::string subcommand = argv[0];
456 int ret = -1;
457
458 if (subcommand == "raw")
459 {
460 ret = ncsiCommandRaw(globalOptions, argc, argv);
461 }
462 else if (subcommand == "oem")
463 {
464 ret = ncsiCommandOEM(globalOptions, argc, argv);
465 }
466 else
467 {
468 std::cerr << "Unknown subcommand '" << subcommand << "'\n";
469 print_usage(progname);
470 }
471
472 return ret ? EXIT_FAILURE : EXIT_SUCCESS;
473 }
474