tools: ynltool: create skeleton for the C command

Based on past discussions it seems like integration of YNL into
iproute2 is unlikely. YNL itself is not great as a C library,
since it has no backward compat (we routinely change types).

Most of the operations can be performed with the generic Python
CLI directly. There is, however, a handful of operations where
summarization of kernel output is very useful (mostly related
to stats: page-pool, qstat).

Create a command (inspired by bpftool, I think it stood the test
of time reasonably well) to be able to plug the subcommands into.

Link: https://lore.kernel.org/1754895902-8790-1-git-send-email-ernis@linux.microsoft.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Link: https://patch.msgid.link/20251107162227.980672-2-kuba@kernel.org
Acked-by: Stanislav Fomichev <sdf@fomichev.me>
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
This commit is contained in:
Jakub Kicinski
2025-11-07 08:22:24 -08:00
committed by Paolo Abeni
parent 8da7bea7db
commit b02d229013
7 changed files with 720 additions and 1 deletions

View File

@@ -12,10 +12,11 @@ endif
libdir ?= $(prefix)/$(libdir_relative)
includedir ?= $(prefix)/include
SUBDIRS = lib generated samples
SUBDIRS = lib generated samples ynltool
all: $(SUBDIRS) libynl.a
ynltool: | lib generated libynl.a
samples: | lib generated
libynl.a: | lib generated
@echo -e "\tAR $@"

1
tools/net/ynl/ynltool/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
ynltool

View File

@@ -0,0 +1,52 @@
# SPDX-License-Identifier: GPL-2.0-only
include ../Makefile.deps
INSTALL ?= install
prefix ?= /usr
CC := gcc
CFLAGS := -Wall -Wextra -Werror -O2
ifeq ("$(DEBUG)","1")
CFLAGS += -g -fsanitize=address -fsanitize=leak -static-libasan
endif
CFLAGS += -I../lib
SRC_VERSION := \
$(shell make --no-print-directory -sC ../../../.. kernelversion || \
echo "unknown")
CFLAGS += -DSRC_VERSION='"$(SRC_VERSION)"'
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,$(OUTPUT)%.o,$(SRCS))
YNLTOOL := $(OUTPUT)ynltool
include $(wildcard *.d)
all: $(YNLTOOL)
Q = @
$(YNLTOOL): $(OBJS)
$(Q)echo -e "\tLINK $@"
$(Q)$(CC) $(CFLAGS) -o $@ $(OBJS)
%.o: %.c main.h json_writer.h
$(Q)echo -e "\tCC $@"
$(Q)$(COMPILE.c) -MMD -c -o $@ $<
clean:
rm -f *.o *.d *~
distclean: clean
rm -f $(YNLTOOL)
bindir ?= /usr/bin
install: $(YNLTOOL)
install -m 0755 $(YNLTOOL) $(DESTDIR)$(bindir)/$(YNLTOOL)
.PHONY: all clean distclean
.DEFAULT_GOAL=all

View File

@@ -0,0 +1,288 @@
// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
/*
* Simple streaming JSON writer
*
* This takes care of the annoying bits of JSON syntax like the commas
* after elements
*
* Authors: Stephen Hemminger <stephen@networkplumber.org>
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdarg.h>
#include <assert.h>
#include <malloc.h>
#include <inttypes.h>
#include <stdint.h>
#include "json_writer.h"
struct json_writer {
FILE *out;
unsigned depth;
bool pretty;
char sep;
};
static void jsonw_indent(json_writer_t *self)
{
unsigned i;
for (i = 0; i < self->depth; ++i)
fputs(" ", self->out);
}
static void jsonw_eol(json_writer_t *self)
{
if (!self->pretty)
return;
putc('\n', self->out);
jsonw_indent(self);
}
static void jsonw_eor(json_writer_t *self)
{
if (self->sep != '\0')
putc(self->sep, self->out);
self->sep = ',';
}
static void jsonw_puts(json_writer_t *self, const char *str)
{
putc('"', self->out);
for (; *str; ++str)
switch (*str) {
case '\t':
fputs("\\t", self->out);
break;
case '\n':
fputs("\\n", self->out);
break;
case '\r':
fputs("\\r", self->out);
break;
case '\f':
fputs("\\f", self->out);
break;
case '\b':
fputs("\\b", self->out);
break;
case '\\':
fputs("\\\\", self->out);
break;
case '"':
fputs("\\\"", self->out);
break;
default:
putc(*str, self->out);
}
putc('"', self->out);
}
json_writer_t *jsonw_new(FILE *f)
{
json_writer_t *self = malloc(sizeof(*self));
if (self) {
self->out = f;
self->depth = 0;
self->pretty = false;
self->sep = '\0';
}
return self;
}
void jsonw_destroy(json_writer_t **self_p)
{
json_writer_t *self = *self_p;
assert(self->depth == 0);
fputs("\n", self->out);
fflush(self->out);
free(self);
*self_p = NULL;
}
void jsonw_pretty(json_writer_t *self, bool on)
{
self->pretty = on;
}
void jsonw_reset(json_writer_t *self)
{
assert(self->depth == 0);
self->sep = '\0';
}
static void jsonw_begin(json_writer_t *self, int c)
{
jsonw_eor(self);
putc(c, self->out);
++self->depth;
self->sep = '\0';
}
static void jsonw_end(json_writer_t *self, int c)
{
assert(self->depth > 0);
--self->depth;
if (self->sep != '\0')
jsonw_eol(self);
putc(c, self->out);
self->sep = ',';
}
void jsonw_name(json_writer_t *self, const char *name)
{
jsonw_eor(self);
jsonw_eol(self);
self->sep = '\0';
jsonw_puts(self, name);
putc(':', self->out);
if (self->pretty)
putc(' ', self->out);
}
void jsonw_vprintf_enquote(json_writer_t *self, const char *fmt, va_list ap)
{
jsonw_eor(self);
putc('"', self->out);
vfprintf(self->out, fmt, ap);
putc('"', self->out);
}
void jsonw_printf(json_writer_t *self, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
jsonw_eor(self);
vfprintf(self->out, fmt, ap);
va_end(ap);
}
void jsonw_start_object(json_writer_t *self)
{
jsonw_begin(self, '{');
}
void jsonw_end_object(json_writer_t *self)
{
jsonw_end(self, '}');
}
void jsonw_start_array(json_writer_t *self)
{
jsonw_begin(self, '[');
}
void jsonw_end_array(json_writer_t *self)
{
jsonw_end(self, ']');
}
void jsonw_string(json_writer_t *self, const char *value)
{
jsonw_eor(self);
jsonw_puts(self, value);
}
void jsonw_bool(json_writer_t *self, bool val)
{
jsonw_printf(self, "%s", val ? "true" : "false");
}
void jsonw_null(json_writer_t *self)
{
jsonw_printf(self, "null");
}
void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num)
{
jsonw_printf(self, fmt, num);
}
void jsonw_float(json_writer_t *self, double num)
{
jsonw_printf(self, "%g", num);
}
void jsonw_hu(json_writer_t *self, unsigned short num)
{
jsonw_printf(self, "%hu", num);
}
void jsonw_uint(json_writer_t *self, uint64_t num)
{
jsonw_printf(self, "%"PRIu64, num);
}
void jsonw_lluint(json_writer_t *self, unsigned long long int num)
{
jsonw_printf(self, "%llu", num);
}
void jsonw_int(json_writer_t *self, int64_t num)
{
jsonw_printf(self, "%"PRId64, num);
}
void jsonw_string_field(json_writer_t *self, const char *prop, const char *val)
{
jsonw_name(self, prop);
jsonw_string(self, val);
}
void jsonw_bool_field(json_writer_t *self, const char *prop, bool val)
{
jsonw_name(self, prop);
jsonw_bool(self, val);
}
void jsonw_float_field(json_writer_t *self, const char *prop, double val)
{
jsonw_name(self, prop);
jsonw_float(self, val);
}
void jsonw_float_field_fmt(json_writer_t *self,
const char *prop,
const char *fmt,
double val)
{
jsonw_name(self, prop);
jsonw_float_fmt(self, fmt, val);
}
void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num)
{
jsonw_name(self, prop);
jsonw_uint(self, num);
}
void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num)
{
jsonw_name(self, prop);
jsonw_hu(self, num);
}
void jsonw_lluint_field(json_writer_t *self,
const char *prop,
unsigned long long int num)
{
jsonw_name(self, prop);
jsonw_lluint(self, num);
}
void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num)
{
jsonw_name(self, prop);
jsonw_int(self, num);
}
void jsonw_null_field(json_writer_t *self, const char *prop)
{
jsonw_name(self, prop);
jsonw_null(self);
}

View File

@@ -0,0 +1,75 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/*
* Simple streaming JSON writer
*
* This takes care of the annoying bits of JSON syntax like the commas
* after elements
*
* Authors: Stephen Hemminger <stephen@networkplumber.org>
*/
#ifndef _JSON_WRITER_H_
#define _JSON_WRITER_H_
#include <stdbool.h>
#include <stdint.h>
#include <stdarg.h>
#include <stdio.h>
/* Opaque class structure */
typedef struct json_writer json_writer_t;
/* Create a new JSON stream */
json_writer_t *jsonw_new(FILE *f);
/* End output to JSON stream */
void jsonw_destroy(json_writer_t **self_p);
/* Cause output to have pretty whitespace */
void jsonw_pretty(json_writer_t *self, bool on);
/* Reset separator to create new JSON */
void jsonw_reset(json_writer_t *self);
/* Add property name */
void jsonw_name(json_writer_t *self, const char *name);
/* Add value */
void __attribute__((format(printf, 2, 0))) jsonw_vprintf_enquote(json_writer_t *self,
const char *fmt,
va_list ap);
void __attribute__((format(printf, 2, 3))) jsonw_printf(json_writer_t *self,
const char *fmt, ...);
void jsonw_string(json_writer_t *self, const char *value);
void jsonw_bool(json_writer_t *self, bool value);
void jsonw_float(json_writer_t *self, double number);
void jsonw_float_fmt(json_writer_t *self, const char *fmt, double num);
void jsonw_uint(json_writer_t *self, uint64_t number);
void jsonw_hu(json_writer_t *self, unsigned short number);
void jsonw_int(json_writer_t *self, int64_t number);
void jsonw_null(json_writer_t *self);
void jsonw_lluint(json_writer_t *self, unsigned long long int num);
/* Useful Combinations of name and value */
void jsonw_string_field(json_writer_t *self, const char *prop, const char *val);
void jsonw_bool_field(json_writer_t *self, const char *prop, bool value);
void jsonw_float_field(json_writer_t *self, const char *prop, double num);
void jsonw_uint_field(json_writer_t *self, const char *prop, uint64_t num);
void jsonw_hu_field(json_writer_t *self, const char *prop, unsigned short num);
void jsonw_int_field(json_writer_t *self, const char *prop, int64_t num);
void jsonw_null_field(json_writer_t *self, const char *prop);
void jsonw_lluint_field(json_writer_t *self, const char *prop,
unsigned long long int num);
void jsonw_float_field_fmt(json_writer_t *self, const char *prop,
const char *fmt, double val);
/* Collections */
void jsonw_start_object(json_writer_t *self);
void jsonw_end_object(json_writer_t *self);
void jsonw_start_array(json_writer_t *self);
void jsonw_end_array(json_writer_t *self);
/* Override default exception handling */
typedef void (jsonw_err_handler_fn)(const char *);
#endif /* _JSON_WRITER_H_ */

View File

@@ -0,0 +1,240 @@
// SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
/* Copyright Meta Platforms, Inc. and affiliates */
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "main.h"
const char *bin_name;
static int last_argc;
static char **last_argv;
static int (*last_do_help)(int argc, char **argv);
json_writer_t *json_wtr;
bool pretty_output;
bool json_output;
static void __attribute__((noreturn)) clean_and_exit(int i)
{
if (json_output)
jsonw_destroy(&json_wtr);
exit(i);
}
void usage(void)
{
last_do_help(last_argc - 1, last_argv + 1);
clean_and_exit(-1);
}
static int do_help(int argc __attribute__((unused)),
char **argv __attribute__((unused)))
{
if (json_output) {
jsonw_null(json_wtr);
return 0;
}
fprintf(stderr,
"Usage: %s [OPTIONS] OBJECT { COMMAND | help }\n"
" %s version\n"
"\n"
" OBJECT := { }\n"
" " HELP_SPEC_OPTIONS "\n"
"",
bin_name, bin_name);
return 0;
}
static int do_version(int argc __attribute__((unused)),
char **argv __attribute__((unused)))
{
if (json_output) {
jsonw_start_object(json_wtr);
jsonw_name(json_wtr, "version");
jsonw_printf(json_wtr, SRC_VERSION);
jsonw_end_object(json_wtr);
} else {
printf("%s " SRC_VERSION "\n", bin_name);
}
return 0;
}
static const struct cmd commands[] = {
{ "help", do_help },
{ "version", do_version },
{ 0 }
};
int cmd_select(const struct cmd *cmds, int argc, char **argv,
int (*help)(int argc, char **argv))
{
unsigned int i;
last_argc = argc;
last_argv = argv;
last_do_help = help;
if (argc < 1 && cmds[0].func)
return cmds[0].func(argc, argv);
for (i = 0; cmds[i].cmd; i++) {
if (is_prefix(*argv, cmds[i].cmd)) {
if (!cmds[i].func) {
p_err("command '%s' is not available", cmds[i].cmd);
return -1;
}
return cmds[i].func(argc - 1, argv + 1);
}
}
help(argc - 1, argv + 1);
return -1;
}
bool is_prefix(const char *pfx, const char *str)
{
if (!pfx)
return false;
if (strlen(str) < strlen(pfx))
return false;
return !memcmp(str, pfx, strlen(pfx));
}
/* Last argument MUST be NULL pointer */
int detect_common_prefix(const char *arg, ...)
{
unsigned int count = 0;
const char *ref;
char msg[256];
va_list ap;
snprintf(msg, sizeof(msg), "ambiguous prefix: '%s' could be '", arg);
va_start(ap, arg);
while ((ref = va_arg(ap, const char *))) {
if (!is_prefix(arg, ref))
continue;
count++;
if (count > 1)
strncat(msg, "' or '", sizeof(msg) - strlen(msg) - 1);
strncat(msg, ref, sizeof(msg) - strlen(msg) - 1);
}
va_end(ap);
strncat(msg, "'", sizeof(msg) - strlen(msg) - 1);
if (count >= 2) {
p_err("%s", msg);
return -1;
}
return 0;
}
void p_err(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (json_output) {
jsonw_start_object(json_wtr);
jsonw_name(json_wtr, "error");
jsonw_vprintf_enquote(json_wtr, fmt, ap);
jsonw_end_object(json_wtr);
} else {
fprintf(stderr, "Error: ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
}
va_end(ap);
}
void p_info(const char *fmt, ...)
{
va_list ap;
if (json_output)
return;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
}
int main(int argc, char **argv)
{
static const struct option options[] = {
{ "json", no_argument, NULL, 'j' },
{ "help", no_argument, NULL, 'h' },
{ "pretty", no_argument, NULL, 'p' },
{ "version", no_argument, NULL, 'V' },
{ 0 }
};
bool version_requested = false;
int opt, ret;
setlinebuf(stdout);
last_do_help = do_help;
pretty_output = false;
json_output = false;
bin_name = "ynltool";
opterr = 0;
while ((opt = getopt_long(argc, argv, "Vhjp",
options, NULL)) >= 0) {
switch (opt) {
case 'V':
version_requested = true;
break;
case 'h':
return do_help(argc, argv);
case 'p':
pretty_output = true;
/* fall through */
case 'j':
if (!json_output) {
json_wtr = jsonw_new(stdout);
if (!json_wtr) {
p_err("failed to create JSON writer");
return -1;
}
json_output = true;
}
jsonw_pretty(json_wtr, pretty_output);
break;
default:
p_err("unrecognized option '%s'", argv[optind - 1]);
if (json_output)
clean_and_exit(-1);
else
usage();
}
}
argc -= optind;
argv += optind;
if (argc < 0)
usage();
if (version_requested)
ret = do_version(argc, argv);
else
ret = cmd_select(commands, argc, argv, do_help);
if (json_output)
jsonw_destroy(&json_wtr);
return ret;
}

View File

@@ -0,0 +1,62 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright (C) 2017-2018 Netronome Systems, Inc. */
/* Copyright Meta Platforms, Inc. and affiliates */
#ifndef __YNLTOOL_H
#define __YNLTOOL_H
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include "json_writer.h"
#define NEXT_ARG() ({ argc--; argv++; if (argc < 0) usage(); })
#define NEXT_ARGP() ({ (*argc)--; (*argv)++; if (*argc < 0) usage(); })
#define BAD_ARG() ({ p_err("what is '%s'?", *argv); -1; })
#define GET_ARG() ({ argc--; *argv++; })
#define REQ_ARGS(cnt) \
({ \
int _cnt = (cnt); \
bool _res; \
\
if (argc < _cnt) { \
p_err("'%s' needs at least %d arguments, %d found", \
argv[-1], _cnt, argc); \
_res = false; \
} else { \
_res = true; \
} \
_res; \
})
#define HELP_SPEC_OPTIONS \
"OPTIONS := { {-j|--json} [{-p|--pretty}] }"
extern const char *bin_name;
extern json_writer_t *json_wtr;
extern bool json_output;
extern bool pretty_output;
void __attribute__((format(printf, 1, 2))) p_err(const char *fmt, ...);
void __attribute__((format(printf, 1, 2))) p_info(const char *fmt, ...);
bool is_prefix(const char *pfx, const char *str);
int detect_common_prefix(const char *arg, ...);
void usage(void) __attribute__((noreturn));
struct cmd {
const char *cmd;
int (*func)(int argc, char **argv);
};
int cmd_select(const struct cmd *cmds, int argc, char **argv,
int (*help)(int argc, char **argv));
#endif /* __YNLTOOL_H */