]>
git.proxmox.com Git - mirror_frr.git/blob - python/xref2vtysh.py
1 # SPDX-License-Identifier: GPL-2.0-or-later
2 # FRR xref vtysh command extraction
4 # Copyright (C) 2022 David Lamparter for NetDEF, Inc.
7 Generate vtysh_cmd.c from frr .xref file(s).
9 This can run either standalone or as part of xrelfo. The latter saves a
10 non-negligible amount of time (0.5s on average systems, more on e.g. slow ARMs)
11 since serializing and deserializing JSON is a significant bottleneck in this.
19 from collections
import defaultdict
25 import ujson
as json
# type: ignore
29 frr_top_src
= os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
)))
31 # vtysh needs to know which daemon(s) to send commands to. For lib/, this is
32 # not quite obvious...
35 "lib/agentx.c": "VTYSH_ISISD|VTYSH_RIPD|VTYSH_OSPFD|VTYSH_OSPF6D|VTYSH_BGPD|VTYSH_ZEBRA",
36 "lib/filter.c": "VTYSH_ACL",
37 "lib/filter_cli.c": "VTYSH_ACL",
38 "lib/if.c": "VTYSH_INTERFACE",
39 "lib/keychain.c": "VTYSH_RIPD|VTYSH_EIGRPD|VTYSH_OSPF6D",
40 "lib/mgmt_be_client.c": "VTYSH_STATICD",
41 "lib/mgmt_fe_client.c": "VTYSH_MGMTD",
42 "lib/lib_vty.c": "VTYSH_ALL",
43 "lib/log_vty.c": "VTYSH_ALL",
44 "lib/nexthop_group.c": "VTYSH_NH_GROUP",
45 "lib/resolver.c": "VTYSH_NHRPD|VTYSH_BGPD",
46 "lib/routemap.c": "VTYSH_RMAP",
47 "lib/routemap_cli.c": "VTYSH_RMAP",
48 "lib/spf_backoff.c": "VTYSH_ISISD",
49 "lib/event.c": "VTYSH_ALL",
50 "lib/vrf.c": "VTYSH_VRF",
51 "lib/vty.c": "VTYSH_ALL",
54 vtysh_cmd_head
= """/* autogenerated file, DO NOT EDIT! */
60 #include "vtysh/vtysh.h"
63 if sys
.stderr
.isatty():
65 _fmt_green
= "\033[32m"
68 _fmt_red
= _fmt_green
= _fmt_clear
= ""
71 def c_escape(text
: str) -> str:
73 Escape string for output into C source code.
75 Handles only what's needed here. CLI strings and help text don't contain
76 weird special characters.
78 return text
.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
81 class NodeDict(defaultdict
):
83 CLI node ID (integer) -> dict of commands in that node.
86 nodenames
= {} # Dict[int, str]
89 super().__init
__(dict)
91 def items_named(self
):
92 for k
, v
in self
.items():
93 yield self
.nodename(k
), v
96 def nodename(cls
, nodeid
: int) -> str:
97 return cls
.nodenames
.get(nodeid
, str(nodeid
))
100 def load_nodenames(cls
):
101 with
open(os
.path
.join(frr_top_src
, "lib", "command.h"), "r") as fd
:
102 command_h
= fd
.read()
104 nodes
= re
.search(r
"enum\s+node_type\s+\{(.*?)\}", command_h
, re
.S
)
107 "regex failed to match on lib/command.h (to get CLI node names)"
110 text
= nodes
.group(1)
111 text
= re
.sub(r
"/\*.*?\*/", "", text
, flags
=re
.S
)
112 text
= re
.sub(r
"//.*?$", "", text
, flags
=re
.M
)
113 text
= text
.replace(",", " ")
116 for i
, name
in enumerate(text
):
117 cls
.nodenames
[i
] = name
122 CLI command definition.
124 - one DEFUN creates at most one of these, even if the same command is
125 installed in multiple CLI nodes (e.g. BGP address-family nodes)
126 - for each CLI node, commands with the same CLI string are merged. This
127 is *almost* irrelevant - ospfd & ospf6d define some identical commands
128 in the route-map node. Those must be merged for things to work
132 all_defs
= [] # List[CommandEntry]
135 def __init__(self
, origin
, name
, spec
):
139 self
._registered
= False
141 self
.cmd
= spec
["string"]
142 self
._cmd
_normalized
= self
.normalize_cmd(self
.cmd
)
144 self
.hidden
= "hidden" in spec
.get("attrs", [])
145 self
.daemons
= self
._get
_daemons
()
147 self
.doclines
= self
._spec
["doc"].splitlines(keepends
=True)
148 if not self
.doclines
[-1].endswith("\n"):
149 self
.warn_loc("docstring does not end with \\n")
151 def warn_loc(self
, wtext
, nodename
=None):
153 Print warning with parseable (compiler style) location
155 Matching the way compilers emit file/lineno means editors/IDE can
156 identify / jump to the error location.
160 prefix
= ": [%s] %s:" % (nodename
, self
.name
)
162 prefix
= ": %s:" % (self
.name
,)
164 for line
in wtext
.rstrip("\n").split("\n"):
168 self
._spec
["defun"]["file"],
169 self
._spec
["defun"]["line"],
176 CommandEntry
.warn_counter
+= 1
178 def _get_daemons(self
):
179 path
= pathlib
.Path(self
.origin
)
180 if path
.name
== "vtysh":
183 defun_file
= os
.path
.relpath(self
._spec
["defun"]["file"], frr_top_src
)
184 defun_path
= pathlib
.Path(defun_file
)
186 if defun_path
.parts
[0] != "lib":
187 if "." not in path
.name
:
188 # daemons don't have dots in their filename
189 return {"VTYSH_" + path
.name
.upper()}
191 # loadable modules - use directory name to determine daemon
192 return {"VTYSH_" + path
.parts
[-2].upper()}
194 if defun_file
in daemon_flags
:
195 return {daemon_flags
[defun_file
]}
197 v6_cmd
= "ipv6" in self
.name
198 if defun_file
== "lib/plist.c":
201 "VTYSH_RIPNGD|VTYSH_OSPF6D|VTYSH_BGPD|VTYSH_ZEBRA|VTYSH_PIM6D|VTYSH_BABELD|VTYSH_ISISD|VTYSH_FABRICD"
205 "VTYSH_RIPD|VTYSH_OSPFD|VTYSH_BGPD|VTYSH_ZEBRA|VTYSH_PIMD|VTYSH_EIGRPD|VTYSH_BABELD|VTYSH_ISISD|VTYSH_FABRICD"
208 if defun_file
== "lib/if_rmap.c":
210 return {"VTYSH_RIPNGD"}
212 return {"VTYSH_RIPD"}
217 return "<CommandEntry %s: %r>" % (self
.name
, self
.cmd
)
220 """Track DEFUNs so each is only output once."""
221 if not self
._registered
:
222 self
.all_defs
.append(self
)
223 self
._registered
= True
226 def merge(self
, other
, nodename
):
227 if self
._cmd
_normalized
!= other
._cmd
_normalized
:
229 "command definition mismatch, first definied as:\n%r" % (self
.cmd
,),
232 other
.warn_loc("later defined as:\n%r" % (other
.cmd
,), nodename
=nodename
)
234 if self
._spec
["doc"] != other
._spec
["doc"]:
236 "help string mismatch, first defined here (-)", nodename
=nodename
239 "later defined here (+)\nnote: both commands define %r in same node (%s)"
240 % (self
.cmd
, nodename
),
245 for diffline
in d
.compare(self
.doclines
, other
.doclines
):
246 if diffline
.startswith(" "):
248 if diffline
.startswith("+ "):
249 diffline
= _fmt_green
+ diffline
250 elif diffline
.startswith("- "):
251 diffline
= _fmt_red
+ diffline
252 sys
.stderr
.write("\t" + diffline
.rstrip("\n") + _fmt_clear
+ "\n")
254 if self
.hidden
!= other
.hidden
:
256 "hidden flag mismatch, first %r here" % (self
.hidden
,),
260 "later %r here (+)\nnote: both commands define %r in same node (%s)"
261 % (other
.hidden
, self
.cmd
, nodename
),
265 # ensure name is deterministic regardless of input DEFUN order
266 self
.name
= min([self
.name
, other
.name
], key
=lambda i
: (len(i
), i
))
267 self
.daemons
.update(other
.daemons
)
270 doc
= "\n".join(['\t"%s"' % c_escape(line
) for line
in self
.doclines
])
271 defsh
= "DEFSH_HIDDEN" if self
.hidden
else "DEFSH"
273 # make daemon list deterministic
275 for daemon
in self
.daemons
:
276 daemons
.update(daemon
.split("|"))
277 daemon_str
= "|".join(sorted(daemons
))
291 # accept slightly different command definitions that result in the same command
292 re_collapse_ws
= re
.compile(r
"\s+")
293 re_remove_varnames
= re
.compile(r
"\$[a-z][a-z0-9_]*")
296 def normalize_cmd(cls
, cmd
):
298 cmd
= cls
.re_collapse_ws
.sub(" ", cmd
)
299 cmd
= cls
.re_remove_varnames
.sub("", cmd
)
303 def process(cls
, nodes
, name
, origin
, spec
):
304 if "nosh" in spec
.get("attrs", []):
306 if origin
== "vtysh/vtysh":
309 if origin
== "isisd/fabricd":
310 # dirty workaround :(
311 name
= "fabricd_" + name
313 entry
= cls(origin
, name
, spec
)
314 if not entry
.daemons
:
317 for nodedata
in spec
.get("nodes", []):
318 node
= nodes
[nodedata
["node"]]
319 if entry
._cmd
_normalized
not in node
:
320 node
[entry
._cmd
_normalized
] = entry
.register()
322 node
[entry
._cmd
_normalized
].merge(
323 entry
, nodes
.nodename(nodedata
["node"])
330 mgmtname
= "mgmtd/libmgmt_be_nb.la"
331 for cmd_name
, origins
in xref
.get("cli", {}).items():
332 # If mgmtd has a yang version of a CLI command, make it the only daemon
333 # to handle it. For now, daemons can still be compiling their cmds into the
334 # binaries to allow for running standalone with CLI config files. When they
335 # do this they will also be present in the xref file, but we want to ignore
337 if "yang" in origins
.get(mgmtname
, {}).get("attrs", []):
338 CommandEntry
.process(nodes
, cmd_name
, mgmtname
, origins
[mgmtname
])
341 for origin
, spec
in origins
.items():
342 CommandEntry
.process(nodes
, cmd_name
, origin
, spec
)
346 def output_defs(cls
, ofd
):
347 for entry
in sorted(cls
.all_defs
, key
=lambda i
: i
.name
):
348 ofd
.write(entry
.get_def())
351 def output_install(cls
, ofd
, nodes
):
352 ofd
.write("\nvoid vtysh_init_cmd(void)\n{\n")
354 for name
, items
in sorted(nodes
.items_named()):
355 for item
in sorted(items
.values(), key
=lambda i
: i
.name
):
356 ofd
.write("\tinstall_element(%s, &%s_vtysh);\n" % (name
, item
.name
))
361 def run(cls
, xref
, ofd
):
362 ofd
.write(vtysh_cmd_head
)
364 NodeDict
.load_nodenames()
365 nodes
= cls
.load(xref
)
367 cls
.output_install(ofd
, nodes
)
371 argp
= argparse
.ArgumentParser(description
="FRR xref to vtysh defs")
373 "xreffile", metavar
="XREFFILE", type=str, help=".xref file to read"
375 argp
.add_argument("-Werror", action
="store_const", const
=True)
376 args
= argp
.parse_args()
378 with
open(args
.xreffile
, "r") as fd
:
381 CommandEntry
.run(data
, sys
.stdout
)
383 if args
.Werror
and CommandEntry
.warn_counter
:
387 if __name__
== "__main__":