xref: /openbmc/phosphor-networkd/src/ncsi_cmd.cpp (revision ca9d86770bb21abebda358087f521bb8904ed974)
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