mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-08-12 11:18:59 +00:00
lib: add ability to dump cli mode graph
The grammar sandbox has had the ability to dump individual commands as DOT graphs, but now that generalized DOT support is present it's trivial to extend this to entire submodes. This is quite useful for visualizing the CLI space when debugging CLI errors. Signed-off-by: Quentin Young <qlyoung@cumulusnetworks.com>
This commit is contained in:
parent
8f2a4d3047
commit
26fbe47294
@ -49,6 +49,29 @@ DEFINE_MTYPE(LIB, HOST, "Host config")
|
|||||||
DEFINE_MTYPE(LIB, STRVEC, "String vector")
|
DEFINE_MTYPE(LIB, STRVEC, "String vector")
|
||||||
DEFINE_MTYPE(LIB, COMPLETION, "Completion item")
|
DEFINE_MTYPE(LIB, COMPLETION, "Completion item")
|
||||||
|
|
||||||
|
#define item(x) \
|
||||||
|
{ \
|
||||||
|
x, #x \
|
||||||
|
}
|
||||||
|
|
||||||
|
/* clang-format off */
|
||||||
|
const struct message tokennames[] = {
|
||||||
|
item(WORD_TKN),
|
||||||
|
item(VARIABLE_TKN),
|
||||||
|
item(RANGE_TKN),
|
||||||
|
item(IPV4_TKN),
|
||||||
|
item(IPV4_PREFIX_TKN),
|
||||||
|
item(IPV6_TKN),
|
||||||
|
item(IPV6_PREFIX_TKN),
|
||||||
|
item(MAC_TKN),
|
||||||
|
item(MAC_PREFIX_TKN),
|
||||||
|
item(FORK_TKN),
|
||||||
|
item(JOIN_TKN),
|
||||||
|
item(START_TKN),
|
||||||
|
item(END_TKN),
|
||||||
|
{0},
|
||||||
|
};
|
||||||
|
|
||||||
const char *node_names[] = {
|
const char *node_names[] = {
|
||||||
"auth", // AUTH_NODE,
|
"auth", // AUTH_NODE,
|
||||||
"view", // VIEW_NODE,
|
"view", // VIEW_NODE,
|
||||||
@ -121,6 +144,7 @@ const char *node_names[] = {
|
|||||||
"bgp ipv6 flowspec", /* BGP_FLOWSPECV6_NODE
|
"bgp ipv6 flowspec", /* BGP_FLOWSPECV6_NODE
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
/* clang-format on */
|
||||||
|
|
||||||
/* Command vector which includes some level of command lists. Normally
|
/* Command vector which includes some level of command lists. Normally
|
||||||
each daemon maintains each own cmdvec. */
|
each daemon maintains each own cmdvec. */
|
||||||
@ -1566,6 +1590,21 @@ DEFUN (show_commandtree,
|
|||||||
return cmd_list_cmds(vty, argc == 3);
|
return cmd_list_cmds(vty, argc == 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFUN_HIDDEN(show_cli_graph,
|
||||||
|
show_cli_graph_cmd,
|
||||||
|
"show cli graph",
|
||||||
|
SHOW_STR
|
||||||
|
"CLI reflection\n"
|
||||||
|
"Dump current command space as DOT graph\n")
|
||||||
|
{
|
||||||
|
struct cmd_node *cn = vector_slot(cmdvec, vty->node);
|
||||||
|
char *dot = cmd_graph_dump_dot(cn->cmdgraph);
|
||||||
|
|
||||||
|
vty_out(vty, "%s\n", dot);
|
||||||
|
XFREE(MTYPE_TMP, dot);
|
||||||
|
return CMD_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
static int vty_write_config(struct vty *vty)
|
static int vty_write_config(struct vty *vty)
|
||||||
{
|
{
|
||||||
size_t i;
|
size_t i;
|
||||||
@ -2573,6 +2612,7 @@ void install_default(enum node_type node)
|
|||||||
install_element(node, &config_end_cmd);
|
install_element(node, &config_end_cmd);
|
||||||
install_element(node, &config_help_cmd);
|
install_element(node, &config_help_cmd);
|
||||||
install_element(node, &config_list_cmd);
|
install_element(node, &config_list_cmd);
|
||||||
|
install_element(node, &show_cli_graph_cmd);
|
||||||
install_element(node, &find_cmd);
|
install_element(node, &find_cmd);
|
||||||
|
|
||||||
install_element(node, &config_write_cmd);
|
install_element(node, &config_write_cmd);
|
||||||
|
@ -146,6 +146,7 @@ enum node_type {
|
|||||||
};
|
};
|
||||||
|
|
||||||
extern vector cmdvec;
|
extern vector cmdvec;
|
||||||
|
extern const struct message tokennames[];
|
||||||
extern const char *node_names[];
|
extern const char *node_names[];
|
||||||
|
|
||||||
/* Node which has some commands and prompt string and configuration
|
/* Node which has some commands and prompt string and configuration
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
#include <zebra.h>
|
#include <zebra.h>
|
||||||
|
|
||||||
#include "command_graph.h"
|
#include "command_graph.h"
|
||||||
|
#include "command.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
DEFINE_MTYPE_STATIC(LIB, CMD_TOKENS, "Command Tokens")
|
DEFINE_MTYPE_STATIC(LIB, CMD_TOKENS, "Command Tokens")
|
||||||
DEFINE_MTYPE_STATIC(LIB, CMD_DESC, "Command Token Text")
|
DEFINE_MTYPE_STATIC(LIB, CMD_DESC, "Command Token Text")
|
||||||
@ -457,3 +459,92 @@ void cmd_graph_names(struct graph *graph)
|
|||||||
|
|
||||||
cmd_node_names(start, NULL, NULL);
|
cmd_node_names(start, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef BUILDING_CLIPPY
|
||||||
|
|
||||||
|
void cmd_graph_node_print_cb(struct graph_node *gn, struct buffer *buf)
|
||||||
|
{
|
||||||
|
static bool wasend;
|
||||||
|
|
||||||
|
char nbuf[512];
|
||||||
|
struct cmd_token *tok = gn->data;
|
||||||
|
const char *color;
|
||||||
|
|
||||||
|
if (wasend == true) {
|
||||||
|
wasend = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok->type == END_TKN) {
|
||||||
|
wasend = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(nbuf, sizeof(nbuf), " n%p [ shape=box, label=<", gn);
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
snprintf(nbuf, sizeof(nbuf), "<b>%s</b>",
|
||||||
|
lookup_msg(tokennames, tok->type, NULL));
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
if (tok->attr == CMD_ATTR_DEPRECATED)
|
||||||
|
buffer_putstr(buf, " (d)");
|
||||||
|
else if (tok->attr == CMD_ATTR_HIDDEN)
|
||||||
|
buffer_putstr(buf, " (h)");
|
||||||
|
if (tok->text) {
|
||||||
|
if (tok->type == WORD_TKN)
|
||||||
|
snprintf(
|
||||||
|
nbuf, sizeof(nbuf),
|
||||||
|
"<br/>\"<font color=\"#0055ff\" point-size=\"11\"><b>%s</b></font>\"",
|
||||||
|
tok->text);
|
||||||
|
else
|
||||||
|
snprintf(nbuf, sizeof(nbuf), "<br/>%s", tok->text);
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tok->type) {
|
||||||
|
case START_TKN:
|
||||||
|
color = "#ccffcc";
|
||||||
|
break;
|
||||||
|
case FORK_TKN:
|
||||||
|
color = "#aaddff";
|
||||||
|
break;
|
||||||
|
case JOIN_TKN:
|
||||||
|
color = "#ddaaff";
|
||||||
|
break;
|
||||||
|
case WORD_TKN:
|
||||||
|
color = "#ffffff";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = "#ffffff";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
snprintf(nbuf, sizeof(nbuf),
|
||||||
|
">, style = filled, fillcolor = \"%s\" ];\n", color);
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < vector_active(gn->to); i++) {
|
||||||
|
struct graph_node *adj = vector_slot(gn->to, i);
|
||||||
|
|
||||||
|
if (((struct cmd_token *)adj->data)->type == END_TKN) {
|
||||||
|
snprintf(nbuf, sizeof(nbuf), " n%p -> end%p;\n", gn,
|
||||||
|
adj);
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
snprintf(
|
||||||
|
nbuf, sizeof(nbuf),
|
||||||
|
" end%p [ shape=box, label=<end>, style = filled, fillcolor = \"#ffddaa\" ];\n",
|
||||||
|
adj);
|
||||||
|
} else
|
||||||
|
snprintf(nbuf, sizeof(nbuf), " n%p -> n%p;\n", gn,
|
||||||
|
adj);
|
||||||
|
|
||||||
|
buffer_putstr(buf, nbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char *cmd_graph_dump_dot(struct graph *cmdgraph)
|
||||||
|
{
|
||||||
|
struct graph_node *start = vector_slot(cmdgraph->nodes, 0);
|
||||||
|
|
||||||
|
return graph_dump_dot(cmdgraph, start, cmd_graph_node_print_cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* BUILDING_CLIPPY */
|
||||||
|
@ -116,5 +116,21 @@ extern void cmd_graph_parse(struct graph *graph, struct cmd_element *cmd);
|
|||||||
extern void cmd_graph_names(struct graph *graph);
|
extern void cmd_graph_names(struct graph *graph);
|
||||||
extern void cmd_graph_merge(struct graph *old, struct graph *new,
|
extern void cmd_graph_merge(struct graph *old, struct graph *new,
|
||||||
int direction);
|
int direction);
|
||||||
|
/*
|
||||||
|
* Print callback for DOT dumping.
|
||||||
|
*
|
||||||
|
* See graph.h for more details.
|
||||||
|
*/
|
||||||
|
extern void cmd_graph_node_print_cb(struct graph_node *gn, struct buffer *buf);
|
||||||
|
/*
|
||||||
|
* Dump command graph to DOT.
|
||||||
|
*
|
||||||
|
* cmdgraph
|
||||||
|
* A command graph to dump
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* String allocated with MTYPE_TMP representing this graph
|
||||||
|
*/
|
||||||
|
char *cmd_graph_dump_dot(struct graph *cmdgraph);
|
||||||
|
|
||||||
#endif /* _FRR_COMMAND_GRAPH_H */
|
#endif /* _FRR_COMMAND_GRAPH_H */
|
||||||
|
@ -37,9 +37,6 @@ DEFINE_MTYPE_STATIC(LIB, CMD_TOKENS, "Command desc")
|
|||||||
void grammar_sandbox_init(void);
|
void grammar_sandbox_init(void);
|
||||||
void pretty_print_graph(struct vty *vty, struct graph_node *, int, int,
|
void pretty_print_graph(struct vty *vty, struct graph_node *, int, int,
|
||||||
struct graph_node **, size_t);
|
struct graph_node **, size_t);
|
||||||
static void pretty_print_dot(FILE *ofd, unsigned opts, struct graph_node *start,
|
|
||||||
struct graph_node **stack, size_t stackpos,
|
|
||||||
struct graph_node **visited, size_t *visitpos);
|
|
||||||
void init_cmdgraph(struct vty *, struct graph **);
|
void init_cmdgraph(struct vty *, struct graph **);
|
||||||
|
|
||||||
/** shim interface commands **/
|
/** shim interface commands **/
|
||||||
@ -274,23 +271,19 @@ DEFUN (grammar_test_dot,
|
|||||||
".dot filename\n")
|
".dot filename\n")
|
||||||
{
|
{
|
||||||
check_nodegraph();
|
check_nodegraph();
|
||||||
|
|
||||||
struct graph_node *stack[CMD_ARGC_MAX];
|
|
||||||
struct graph_node *visited[CMD_ARGC_MAX * CMD_ARGC_MAX];
|
|
||||||
size_t vpos = 0;
|
|
||||||
|
|
||||||
FILE *ofd = fopen(argv[2]->arg, "w");
|
FILE *ofd = fopen(argv[2]->arg, "w");
|
||||||
|
|
||||||
if (!ofd) {
|
if (!ofd) {
|
||||||
vty_out(vty, "%s: %s\r\n", argv[2]->arg, strerror(errno));
|
vty_out(vty, "%s: %s\r\n", argv[2]->arg, strerror(errno));
|
||||||
return CMD_SUCCESS;
|
return CMD_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(ofd,
|
char *dot = cmd_graph_dump_dot(nodegraph);
|
||||||
"digraph {\n graph [ rankdir = LR ];\n node [ fontname = \"Fira Mono\", fontsize = 9 ];\n\n");
|
|
||||||
pretty_print_dot(ofd, 0, vector_slot(nodegraph->nodes, 0), stack, 0,
|
fprintf(ofd, "%s", dot);
|
||||||
visited, &vpos);
|
|
||||||
fprintf(ofd, "}\n");
|
|
||||||
fclose(ofd);
|
fclose(ofd);
|
||||||
|
XFREE(MTYPE_TMP, dot);
|
||||||
|
|
||||||
return CMD_SUCCESS;
|
return CMD_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -489,24 +482,6 @@ void grammar_sandbox_init(void)
|
|||||||
install_element(ENABLE_NODE, &grammar_access_cmd);
|
install_element(ENABLE_NODE, &grammar_access_cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define item(x) { x, #x }
|
|
||||||
struct message tokennames[] = {item(WORD_TKN), // words
|
|
||||||
item(VARIABLE_TKN), // almost anything
|
|
||||||
item(RANGE_TKN), // integer range
|
|
||||||
item(IPV4_TKN), // IPV4 addresses
|
|
||||||
item(IPV4_PREFIX_TKN), // IPV4 network prefixes
|
|
||||||
item(IPV6_TKN), // IPV6 prefixes
|
|
||||||
item(IPV6_PREFIX_TKN), // IPV6 network prefixes
|
|
||||||
item(MAC_TKN), // MAC address
|
|
||||||
item(MAC_PREFIX_TKN), // MAC address w/ mask
|
|
||||||
|
|
||||||
/* plumbing types */
|
|
||||||
item(FORK_TKN),
|
|
||||||
item(JOIN_TKN),
|
|
||||||
item(START_TKN), // first token in line
|
|
||||||
item(END_TKN), // last token in line
|
|
||||||
{0}};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pretty-prints a graph, assuming it is a tree.
|
* Pretty-prints a graph, assuming it is a tree.
|
||||||
*
|
*
|
||||||
@ -571,89 +546,6 @@ void pretty_print_graph(struct vty *vty, struct graph_node *start, int level,
|
|||||||
vty_out(vty, "\n");
|
vty_out(vty, "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void pretty_print_dot(FILE *ofd, unsigned opts, struct graph_node *start,
|
|
||||||
struct graph_node **stack, size_t stackpos,
|
|
||||||
struct graph_node **visited, size_t *visitpos)
|
|
||||||
{
|
|
||||||
// print this node
|
|
||||||
char tokennum[32];
|
|
||||||
struct cmd_token *tok = start->data;
|
|
||||||
const char *color;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < (*visitpos); i++)
|
|
||||||
if (visited[i] == start)
|
|
||||||
return;
|
|
||||||
visited[(*visitpos)++] = start;
|
|
||||||
if ((*visitpos) == CMD_ARGC_MAX * CMD_ARGC_MAX)
|
|
||||||
return;
|
|
||||||
|
|
||||||
snprintf(tokennum, sizeof(tokennum), "%d?", tok->type);
|
|
||||||
fprintf(ofd, " n%p [ shape=box, label=<", start);
|
|
||||||
|
|
||||||
fprintf(ofd, "<b>%s</b>", lookup_msg(tokennames, tok->type, NULL));
|
|
||||||
if (tok->attr == CMD_ATTR_DEPRECATED)
|
|
||||||
fprintf(ofd, " (d)");
|
|
||||||
else if (tok->attr == CMD_ATTR_HIDDEN)
|
|
||||||
fprintf(ofd, " (h)");
|
|
||||||
if (tok->text) {
|
|
||||||
if (tok->type == WORD_TKN)
|
|
||||||
fprintf(ofd,
|
|
||||||
"<br/>\"<font color=\"#0055ff\" point-size=\"11\"><b>%s</b></font>\"",
|
|
||||||
tok->text);
|
|
||||||
else
|
|
||||||
fprintf(ofd, "<br/>%s", tok->text);
|
|
||||||
}
|
|
||||||
/* if (desc)
|
|
||||||
fprintf(ofd, " ?'%s'", tok->desc); */
|
|
||||||
switch (tok->type) {
|
|
||||||
case START_TKN:
|
|
||||||
color = "#ccffcc";
|
|
||||||
break;
|
|
||||||
case FORK_TKN:
|
|
||||||
color = "#aaddff";
|
|
||||||
break;
|
|
||||||
case JOIN_TKN:
|
|
||||||
color = "#ddaaff";
|
|
||||||
break;
|
|
||||||
case WORD_TKN:
|
|
||||||
color = "#ffffff";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
color = "#ffffff";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fprintf(ofd, ">, style = filled, fillcolor = \"%s\" ];\n", color);
|
|
||||||
|
|
||||||
if (stackpos == CMD_ARGC_MAX)
|
|
||||||
return;
|
|
||||||
stack[stackpos++] = start;
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < vector_active(start->to); i++) {
|
|
||||||
struct graph_node *adj = vector_slot(start->to, i);
|
|
||||||
// if this node is a vararg, just print *
|
|
||||||
if (adj == start) {
|
|
||||||
fprintf(ofd, " n%p -> n%p;\n", start, start);
|
|
||||||
} else if (((struct cmd_token *)adj->data)->type == END_TKN) {
|
|
||||||
// struct cmd_token *et = adj->data;
|
|
||||||
fprintf(ofd, " n%p -> end%p;\n", start, adj);
|
|
||||||
fprintf(ofd,
|
|
||||||
" end%p [ shape=box, label=<end>, style = filled, fillcolor = \"#ffddaa\" ];\n",
|
|
||||||
adj);
|
|
||||||
} else {
|
|
||||||
fprintf(ofd, " n%p -> n%p;\n", start, adj);
|
|
||||||
size_t k;
|
|
||||||
for (k = 0; k < stackpos; k++)
|
|
||||||
if (stack[k] == adj)
|
|
||||||
break;
|
|
||||||
if (k == stackpos) {
|
|
||||||
pretty_print_dot(ofd, opts, adj, stack,
|
|
||||||
stackpos, visited, visitpos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** stuff that should go in command.c + command.h */
|
/** stuff that should go in command.c + command.h */
|
||||||
void init_cmdgraph(struct vty *vty, struct graph **graph)
|
void init_cmdgraph(struct vty *vty, struct graph **graph)
|
||||||
{
|
{
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
#include "ns.h"
|
#include "ns.h"
|
||||||
#include "vrf.h"
|
#include "vrf.h"
|
||||||
#include "libfrr.h"
|
#include "libfrr.h"
|
||||||
|
#include "command_graph.h"
|
||||||
|
|
||||||
DEFINE_MTYPE_STATIC(MVTYSH, VTYSH_CMD, "Vtysh cmd copy")
|
DEFINE_MTYPE_STATIC(MVTYSH, VTYSH_CMD, "Vtysh cmd copy")
|
||||||
|
|
||||||
@ -2957,10 +2958,26 @@ DEFUN(find,
|
|||||||
return CMD_SUCCESS;
|
return CMD_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEFUN_HIDDEN(show_cli_graph_vtysh,
|
||||||
|
show_cli_graph_vtysh_cmd,
|
||||||
|
"show cli graph",
|
||||||
|
SHOW_STR
|
||||||
|
"CLI reflection\n"
|
||||||
|
"Dump current command space as DOT graph\n")
|
||||||
|
{
|
||||||
|
struct cmd_node *cn = vector_slot(cmdvec, vty->node);
|
||||||
|
char *dot = cmd_graph_dump_dot(cn->cmdgraph);
|
||||||
|
|
||||||
|
vty_out(vty, "%s\n", dot);
|
||||||
|
XFREE(MTYPE_TMP, dot);
|
||||||
|
return CMD_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
static void vtysh_install_default(enum node_type node)
|
static void vtysh_install_default(enum node_type node)
|
||||||
{
|
{
|
||||||
install_element(node, &config_list_cmd);
|
install_element(node, &config_list_cmd);
|
||||||
install_element(node, &find_cmd);
|
install_element(node, &find_cmd);
|
||||||
|
install_element(node, &show_cli_graph_vtysh_cmd);
|
||||||
install_element(node, &vtysh_output_file_cmd);
|
install_element(node, &vtysh_output_file_cmd);
|
||||||
install_element(node, &no_vtysh_output_file_cmd);
|
install_element(node, &no_vtysh_output_file_cmd);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user