]>
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/lib_vty.c": "VTYSH_ALL",
41 "lib/log_vty.c": "VTYSH_ALL",
42 "lib/nexthop_group.c": "VTYSH_NH_GROUP",
43 "lib/resolver.c": "VTYSH_NHRPD|VTYSH_BGPD",
44 "lib/routemap.c": "VTYSH_RMAP",
45 "lib/routemap_cli.c": "VTYSH_RMAP",
46 "lib/spf_backoff.c": "VTYSH_ISISD",
47 "lib/thread.c": "VTYSH_ALL",
48 "lib/vrf.c": "VTYSH_VRF",
49 "lib/vty.c": "VTYSH_ALL",
52 vtysh_cmd_head
= """/* autogenerated file, DO NOT EDIT! */
58 #include "vtysh/vtysh.h"
61 if sys
.stderr
.isatty():
63 _fmt_green
= "\033[32m"
66 _fmt_red
= _fmt_green
= _fmt_clear
= ""
69 def c_escape(text
: str) -> str:
71 Escape string for output into C source code.
73 Handles only what's needed here. CLI strings and help text don't contain
74 weird special characters.
76 return text
.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
79 class NodeDict(defaultdict
):
81 CLI node ID (integer) -> dict of commands in that node.
84 nodenames
= {} # Dict[int, str]
87 super().__init
__(dict)
89 def items_named(self
):
90 for k
, v
in self
.items():
91 yield self
.nodename(k
), v
94 def nodename(cls
, nodeid
: int) -> str:
95 return cls
.nodenames
.get(nodeid
, str(nodeid
))
98 def load_nodenames(cls
):
99 with
open(os
.path
.join(frr_top_src
, "lib", "command.h"), "r") as fd
:
100 command_h
= fd
.read()
102 nodes
= re
.search(r
"enum\s+node_type\s+\{(.*?)\}", command_h
, re
.S
)
105 "regex failed to match on lib/command.h (to get CLI node names)"
108 text
= nodes
.group(1)
109 text
= re
.sub(r
"/\*.*?\*/", "", text
, flags
=re
.S
)
110 text
= re
.sub(r
"//.*?$", "", text
, flags
=re
.M
)
111 text
= text
.replace(",", " ")
114 for i
, name
in enumerate(text
):
115 cls
.nodenames
[i
] = name
120 CLI command definition.
122 - one DEFUN creates at most one of these, even if the same command is
123 installed in multiple CLI nodes (e.g. BGP address-family nodes)
124 - for each CLI node, commands with the same CLI string are merged. This
125 is *almost* irrelevant - ospfd & ospf6d define some identical commands
126 in the route-map node. Those must be merged for things to work
130 all_defs
= [] # List[CommandEntry]
133 def __init__(self
, origin
, name
, spec
):
137 self
._registered
= False
139 self
.cmd
= spec
["string"]
140 self
._cmd
_normalized
= self
.normalize_cmd(self
.cmd
)
142 self
.hidden
= "hidden" in spec
.get("attrs", [])
143 self
.daemons
= self
._get
_daemons
()
145 self
.doclines
= self
._spec
["doc"].splitlines(keepends
=True)
146 if not self
.doclines
[-1].endswith("\n"):
147 self
.warn_loc("docstring does not end with \\n")
149 def warn_loc(self
, wtext
, nodename
=None):
151 Print warning with parseable (compiler style) location
153 Matching the way compilers emit file/lineno means editors/IDE can
154 identify / jump to the error location.
158 prefix
= ": [%s] %s:" % (nodename
, self
.name
)
160 prefix
= ": %s:" % (self
.name
,)
162 for line
in wtext
.rstrip("\n").split("\n"):
166 self
._spec
["defun"]["file"],
167 self
._spec
["defun"]["line"],
174 CommandEntry
.warn_counter
+= 1
176 def _get_daemons(self
):
177 path
= pathlib
.Path(self
.origin
)
178 if path
.name
== "vtysh":
181 defun_file
= os
.path
.relpath(self
._spec
["defun"]["file"], frr_top_src
)
182 defun_path
= pathlib
.Path(defun_file
)
184 if defun_path
.parts
[0] != "lib":
185 if "." not in path
.name
:
186 # daemons don't have dots in their filename
187 return {"VTYSH_" + path
.name
.upper()}
189 # loadable modules - use directory name to determine daemon
190 return {"VTYSH_" + path
.parts
[-2].upper()}
192 if defun_file
in daemon_flags
:
193 return {daemon_flags
[defun_file
]}
195 v6_cmd
= "ipv6" in self
.name
196 if defun_file
== "lib/plist.c":
199 "VTYSH_RIPNGD|VTYSH_OSPF6D|VTYSH_BGPD|VTYSH_ZEBRA|VTYSH_PIM6D|VTYSH_BABELD|VTYSH_ISISD|VTYSH_FABRICD"
203 "VTYSH_RIPD|VTYSH_OSPFD|VTYSH_BGPD|VTYSH_ZEBRA|VTYSH_PIMD|VTYSH_EIGRPD|VTYSH_BABELD|VTYSH_ISISD|VTYSH_FABRICD"
206 if defun_file
== "lib/if_rmap.c":
208 return {"VTYSH_RIPNGD"}
210 return {"VTYSH_RIPD"}
215 return "<CommandEntry %s: %r>" % (self
.name
, self
.cmd
)
218 """Track DEFUNs so each is only output once."""
219 if not self
._registered
:
220 self
.all_defs
.append(self
)
221 self
._registered
= True
224 def merge(self
, other
, nodename
):
225 if self
._cmd
_normalized
!= other
._cmd
_normalized
:
227 "command definition mismatch, first definied as:\n%r" % (self
.cmd
,),
230 other
.warn_loc("later defined as:\n%r" % (other
.cmd
,), nodename
=nodename
)
232 if self
._spec
["doc"] != other
._spec
["doc"]:
234 "help string mismatch, first defined here (-)", nodename
=nodename
237 "later defined here (+)\nnote: both commands define %r in same node (%s)"
238 % (self
.cmd
, nodename
),
243 for diffline
in d
.compare(self
.doclines
, other
.doclines
):
244 if diffline
.startswith(" "):
246 if diffline
.startswith("+ "):
247 diffline
= _fmt_green
+ diffline
248 elif diffline
.startswith("- "):
249 diffline
= _fmt_red
+ diffline
250 sys
.stderr
.write("\t" + diffline
.rstrip("\n") + _fmt_clear
+ "\n")
252 if self
.hidden
!= other
.hidden
:
254 "hidden flag mismatch, first %r here" % (self
.hidden
,),
258 "later %r here (+)\nnote: both commands define %r in same node (%s)"
259 % (other
.hidden
, self
.cmd
, nodename
),
263 # ensure name is deterministic regardless of input DEFUN order
264 self
.name
= min([self
.name
, other
.name
], key
=lambda i
: (len(i
), i
))
265 self
.daemons
.update(other
.daemons
)
268 doc
= "\n".join(['\t"%s"' % c_escape(line
) for line
in self
.doclines
])
269 defsh
= "DEFSH_HIDDEN" if self
.hidden
else "DEFSH"
271 # make daemon list deterministic
273 for daemon
in self
.daemons
:
274 daemons
.update(daemon
.split("|"))
275 daemon_str
= "|".join(sorted(daemons
))
289 # accept slightly different command definitions that result in the same command
290 re_collapse_ws
= re
.compile(r
"\s+")
291 re_remove_varnames
= re
.compile(r
"\$[a-z][a-z0-9_]*")
294 def normalize_cmd(cls
, cmd
):
296 cmd
= cls
.re_collapse_ws
.sub(" ", cmd
)
297 cmd
= cls
.re_remove_varnames
.sub("", cmd
)
301 def process(cls
, nodes
, name
, origin
, spec
):
302 if "nosh" in spec
.get("attrs", []):
304 if origin
== "vtysh/vtysh":
307 if origin
== "isisd/fabricd":
308 # dirty workaround :(
309 name
= "fabricd_" + name
311 entry
= cls(origin
, name
, spec
)
312 if not entry
.daemons
:
315 for nodedata
in spec
.get("nodes", []):
316 node
= nodes
[nodedata
["node"]]
317 if entry
._cmd
_normalized
not in node
:
318 node
[entry
._cmd
_normalized
] = entry
.register()
320 node
[entry
._cmd
_normalized
].merge(
321 entry
, nodes
.nodename(nodedata
["node"])
328 for cmd_name
, origins
in xref
.get("cli", {}).items():
329 for origin
, spec
in origins
.items():
330 CommandEntry
.process(nodes
, cmd_name
, origin
, spec
)
334 def output_defs(cls
, ofd
):
335 for entry
in sorted(cls
.all_defs
, key
=lambda i
: i
.name
):
336 ofd
.write(entry
.get_def())
339 def output_install(cls
, ofd
, nodes
):
340 ofd
.write("\nvoid vtysh_init_cmd(void)\n{\n")
342 for name
, items
in sorted(nodes
.items_named()):
343 for item
in sorted(items
.values(), key
=lambda i
: i
.name
):
344 ofd
.write("\tinstall_element(%s, &%s_vtysh);\n" % (name
, item
.name
))
349 def run(cls
, xref
, ofd
):
350 ofd
.write(vtysh_cmd_head
)
352 NodeDict
.load_nodenames()
353 nodes
= cls
.load(xref
)
355 cls
.output_install(ofd
, nodes
)
359 argp
= argparse
.ArgumentParser(description
="FRR xref to vtysh defs")
361 "xreffile", metavar
="XREFFILE", type=str, help=".xref file to read"
363 argp
.add_argument("-Werror", action
="store_const", const
=True)
364 args
= argp
.parse_args()
366 with
open(args
.xreffile
, "r") as fd
:
369 CommandEntry
.run(data
, sys
.stdout
)
371 if args
.Werror
and CommandEntry
.warn_counter
:
375 if __name__
== "__main__":