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