File size: 13,709 Bytes
1bd64c1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | #!/usr/bin/env python3
"""
Parse the FRR Master CLI Command Reference (06_Documentation/FRR_Master_CLI_Command_Reference.json)
into the tool's row schema {os, role, vendor, cat, title, cmd, desc, live, in_docs}.
Input shape (per-row):
{
"command": "router bgp AS-NUMBER view NAME",
"daemon": "bgpd",
"description": "Make a new BGP view...",
"is_documented": true,
"is_running": false,
"source_origin": "bgpd.rst" # or "Docker Active Kernel"
}
The master file is a superset: official FRR docs UNION commands actually observed
running on the local Docker FRR routers (the geshlab clab). We surface that
provenance via two extra row fields:
* `live` β true iff this exact command was observed on a real FRR router
(is_running == true). Drives the "β live" badge in the UI.
* `in_docs` β true iff this command is present in the official FRR RST docs
(is_documented == true).
Output: scripts/frr.json β same envelope as encor.json / junos.json / arista.json
plus the two FRR-specific provenance flags (other vendors' rows simply lack them
and the UI treats them as missing).
Then `parse.py` merges it into ../commands.json on next run.
No third-party deps; Python 3 stdlib only.
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import re
import sys
from collections import Counter
# --- Paths -------------------------------------------------------------------
# Paths are parameterized so the pipeline is reproducible on any machine.
# Defaults derive from __file__ (output) and an overridable docs base (input),
# preserving the original behaviour when run from the repo on the author's box.
HERE = pathlib.Path(__file__).resolve().parent
REPO = HERE.parent
# Base dir holding the upstream FRR reference JSONs. Override with the
# CLI_WORK_DOCS env var or the --input flag to run off another machine.
DOCS_BASE = pathlib.Path(
os.environ.get(
"CLI_WORK_DOCS",
"/Users/georgigaydarov/02_Projects/Network_Automation/VSS_Code_Georgi/"
"06_Documentation",
)
)
DEFAULT_INPUT = DOCS_BASE / "FRR_Master_CLI_Command_Reference.json"
# Older / smaller file kept as a documented fallback if the master one is missing.
LEGACY_INPUT = DOCS_BASE / "FRR_CLI_Command_Reference.json"
# Output lives alongside the other parser outputs in scripts/ (repo-relative).
DEFAULT_OUTPUT = HERE / "frr.json"
# --- Daemon β category map ---------------------------------------------------
# Categories deliberately align with existing categories used by other parsers
# so the Compare view's conceptKey() can line up FRR `router bgp` with Cisco
# `router bgp` and Junos `set protocols bgp`.
DAEMON_TO_CAT: dict[str, str] = {
"bgpd": "BGP",
"bgp-linkstate": "BGP", # variant; stays in BGP bucket
"bmp": "BGP", # BMP is a BGP monitoring feature
"ospfd": "OSPF",
"ospf6d": "OSPFv3",
"isisd": "ISIS",
"fabricd": "OpenFabric",
"pim": "Multicast",
"pimv6": "Multicast",
"ripd": "RIP",
"ripngd": "RIPng",
"ldpd": "MPLS",
"babeld": "Babel",
"eigrpd": "EIGRP",
"bfd": "BFD",
"vrrp": "VRRP",
"vnc": "VNC",
"nhrpd": "NHRP",
"pathd": "Segment Routing",
"pbr": "PBR",
"rpki": "RPKI",
"sharp": "Troubleshooting",
"staticd": "Routing",
"routemap": "Routing", # route-maps are routing policy
"filter": "AAA", # access-lists / prefix-lists policy
"zebra": "Routing",
"ipv6": "Interfaces", # ipv6 nd / interface-level
"basic": "System",
"vtysh": "System",
"mgmtd": "System",
"extlog": "System",
}
# --- Placeholder substitution to make `cmd` paste-runnable -------------------
# `title` keeps the FRR signature with placeholders intact. `cmd` is a filled
# example so the row's Copy button gives the user something runnable.
# Order matters: longer/more specific patterns first.
_PLACEHOLDER_PASSES: list[tuple[re.Pattern, str]] = [
# IPv4 prefix
(re.compile(r"\bA\.B\.C\.D/M\b"), "10.0.0.0/24"),
(re.compile(r"\bA\.B\.C\.D\b"), "10.0.0.1"),
# IPv6
(re.compile(r"\bX:X::X:X/M\b"), "2001:db8::/64"),
(re.compile(r"\bX:X::X:X\b"), "2001:db8::1"),
# MAC
(re.compile(r"\bH\.H\.H\b"), "0050.5681.1234"),
(re.compile(r"\bHH:HH:HH:HH:HH:HH\b"), "00:50:56:81:12:34"),
# Common literal placeholders
(re.compile(r"\bAS-NUMBER\b"), "65001"),
(re.compile(r"\bASN\b"), "65001"),
(re.compile(r"\bROUTER-ID\b"), "1.1.1.1"),
(re.compile(r"\bIFNAME\b"), "eth0"),
(re.compile(r"\bINTERFACE\b"), "eth0"),
(re.compile(r"\bVRF-?NAME\b", re.IGNORECASE),"VRF1"),
(re.compile(r"\bVIEW-?NAME\b", re.IGNORECASE),"VIEW1"),
(re.compile(r"\bRD\b"), "65001:1"),
(re.compile(r"\bRT\b"), "65001:1"),
(re.compile(r"\bCOMMUNITY\b"), "65001:100"),
(re.compile(r"\bLINE\b"), "DESCRIPTION"),
(re.compile(r"\bWORD\b"), "MYWORD"),
(re.compile(r"\bNAME\b"), "MYNAME"),
# Numeric range like (1-600) or (0-4294967295) β middle-ish/first sensible
(re.compile(r"\((\d+)-(\d+)\)"),
lambda m: str(min(int(m.group(2)), max(int(m.group(1)), 10)))),
# Choice groups: `[a | b | c]` β `a` (no brackets); `<a | b>` β `a`
(re.compile(r"\[([^\[\]|]+?)(?:\s*\|\s*[^\[\]]+)?\]"),
lambda m: m.group(1).strip()),
(re.compile(r"<([^<>|]+?)(?:\s*\|\s*[^<>]+)?>"),
lambda m: m.group(1).strip()),
]
def fill_placeholders(cmd: str) -> str:
"""Best-effort substitute placeholders so the cmd is paste-runnable."""
out = cmd
for pat, repl in _PLACEHOLDER_PASSES:
out = pat.sub(repl, out)
return out.strip()
# --- Description tidying -----------------------------------------------------
_RST_DIRECTIVE = re.compile(
r"\n\s*\.\.\s+(code-block|sourcecode|note|warning|tip|seealso)::.*?(?=\n\S|\Z)",
re.DOTALL,
)
_BACKTICK_DOUBLE = re.compile(r"``([^`]+)``")
_BACKTICK_SINGLE = re.compile(r"`([^`]+)`")
def first_sentence(desc: str, cap: int = 220) -> str:
"""Strip RST artifacts and return first 1β2 sentences, capped at `cap`."""
if not desc:
return ""
s = _RST_DIRECTIVE.sub("", desc)
s = _BACKTICK_DOUBLE.sub(r"\1", s)
s = _BACKTICK_SINGLE.sub(r"\1", s)
s = re.sub(r"\s+", " ", s).strip()
# First sentence β first period followed by space/end. Keep up to 2 if short.
parts = re.split(r"(?<=[.!?])\s+", s)
out = parts[0]
if len(out) < 80 and len(parts) > 1:
out = (out + " " + parts[1]).strip()
if len(out) > cap:
out = out[: cap - 1].rstrip() + "β¦"
return out
# --- Role inference ----------------------------------------------------------
# FRR is a routing stack used on Linux hosts / clab / SONiC / Cumulus / VyOS-FRR.
# All commands map to role="router" β the only role FRR ships in.
def role_for(_daemon: str) -> str:
return "router"
def cat_for(daemon: str) -> str:
d = (daemon or "").lower().strip()
if d in DAEMON_TO_CAT:
return DAEMON_TO_CAT[d]
# Fallback by fuzzy match on the daemon name
if "bgp" in d: return "BGP"
if "ospf6" in d: return "OSPFv3"
if "ospf" in d: return "OSPF"
if "isis" in d: return "ISIS"
if "rip" in d: return "RIP"
if "pim" in d or "mld" in d or "igmp" in d: return "Multicast"
if "ldp" in d or "mpls" in d: return "MPLS"
if "bfd" in d: return "BFD"
if "vrrp" in d: return "VRRP"
if "vrf" in d: return "Routing"
return "System"
# --- Main --------------------------------------------------------------------
def main(
input_path: pathlib.Path = DEFAULT_INPUT,
output_path: pathlib.Path = DEFAULT_OUTPUT,
) -> int:
# Pick master file if available, otherwise fall back to the older single-source one.
if not input_path.exists():
if LEGACY_INPUT.exists():
print(f"note: master not found, falling back to {LEGACY_INPUT.name}", file=sys.stderr)
input_path = LEGACY_INPUT
else:
print(f"error: FRR source not found: {input_path}", file=sys.stderr)
return 1
src = json.loads(input_path.read_text(encoding="utf-8"))
if not isinstance(src, list):
print("error: expected top-level list", file=sys.stderr)
return 1
rows: list[dict] = []
seen: set[str] = set()
daemon_counter: Counter = Counter()
cat_counter: Counter = Counter()
prov_counter: Counter = Counter() # (live, in_docs) β n
for entry in src:
cmd_sig = (entry.get("command") or "").strip()
if not cmd_sig:
continue
daemon = (entry.get("daemon") or "").strip().lower()
desc = entry.get("description") or ""
# Master file uses is_documented / is_running / source_origin.
# Legacy file has neither β treat as documented, not live.
in_docs = bool(entry.get("is_documented", "source_file" in entry))
is_running = bool(entry.get("is_running", False))
source_origin = entry.get("source_origin") or entry.get("source_file") or ""
cat = cat_for(daemon)
title = cmd_sig # keep placeholders for the title
cmd = fill_placeholders(cmd_sig) # paste-runnable version
# Build a useful desc. The master file replaces all undocumented-running
# entries with a single boilerplate string β collapse those to a more
# informative provenance breadcrumb.
is_boilerplate = desc.strip().startswith("Undocumented operational")
if is_boilerplate:
short_desc = "" # we'll synthesise below
else:
short_desc = first_sentence(desc)
# Provenance prefix: clarify whether this is a doc command, a live one,
# or both. Engineers reading the card should know if the snippet was
# verified against a real router or only from the RST docs.
prov_bits: list[str] = [cat]
if is_running and in_docs:
prov_bits.append("β doc + live (Docker FRR)")
elif is_running:
prov_bits.append("β live on Docker FRR (undocumented)")
# documented-only rows don't need a marker β that's the default
if daemon and not short_desc:
prov_bits.append(f"daemon: {daemon}")
prov_prefix = " Β· ".join(prov_bits)
if short_desc:
short_desc = f"{prov_prefix} β {short_desc}"
else:
short_desc = prov_prefix
key = re.sub(r"\s+", " ", cmd).lower()
if key in seen:
continue
seen.add(key)
row = {
"os": "frr",
"role": role_for(daemon),
"vendor": "FRR",
"cat": cat,
"title": title,
"cmd": cmd,
"desc": short_desc,
}
# FRR-only provenance flags β other vendors' rows simply omit these.
if is_running:
row["live"] = True
if in_docs:
row["in_docs"] = True
rows.append(row)
daemon_counter[daemon] += 1
cat_counter[cat] += 1
prov_counter[(is_running, in_docs)] += 1
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(rows, indent=1, ensure_ascii=False))
print(f"wrote {len(rows)} FRR rows β {output_path}")
print(f"\nProvenance (running, in_docs) β count:")
for k, n in prov_counter.most_common():
running, docd = k
label = (
"doc + live (verified)" if running and docd else
"live only (undocumented)" if running else
"doc only"
)
print(f" live={int(running):d} doc={int(docd):d} {n:5d} {label}")
print("\nTop daemons:")
for d, n in daemon_counter.most_common(15):
print(f" {d:20s} {n}")
print("\nBy category:")
for c, n in cat_counter.most_common():
print(f" {c:20s} {n}")
# Show 3 sample rows
print("\nSample rows:")
for r in rows[:3]:
print(json.dumps(r, indent=2, ensure_ascii=False))
return 0
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Parse the FRR Master CLI Command Reference into the tool's row "
"schema. Paths default to repo-relative output and an overridable "
"docs base (CLI_WORK_DOCS env var) so the pipeline is reproducible."
)
)
parser.add_argument(
"--input",
type=pathlib.Path,
default=DEFAULT_INPUT,
help=(
"Path to the FRR reference JSON (default: %(default)s). "
"Falls back to FRR_CLI_Command_Reference.json in the same dir if "
"this file is missing."
),
)
parser.add_argument(
"--output",
type=pathlib.Path,
default=DEFAULT_OUTPUT,
help="Path to write the parsed frr.json (default: %(default)s).",
)
return parser.parse_args(argv)
if __name__ == "__main__":
args = parse_args()
sys.exit(main(args.input, args.output))
|