From 1e33cb12bc47dd54bc73412c9636a6b8a9b0cffc Mon Sep 17 00:00:00 2001 From: Matt Vitale Date: Mon, 17 Aug 2026 16:31:37 -0400 Subject: [PATCH 1/3] Add test to validate indentation consistency --- tests/test_template_syntax.py | 59 +++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_template_syntax.py diff --git a/tests/test_template_syntax.py b/tests/test_template_syntax.py new file mode 100644 index 0000000000..382372b4f3 --- /dev/null +++ b/tests/test_template_syntax.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +"""Run syntax tests against all the *.textfsm template files.""" + +import glob +import re + +import pytest +import textfsm + +# Rules and comments inside a state must be indented with exactly 2 spaces. +RE_INDENTED_LINE = re.compile(r"^([ \t]+)(\S)") +VALID_INDENTED_LINES = {(" ", "^"), (" ", "#")} +# States that are valid without being referenced by another rule. +IMPLICIT_STATES = {"Start", "End", "EOF"} + + +def return_template_files(): + """Return a list of all the *.textfsm template files.""" + return glob.glob("./ntc_templates/templates/*.textfsm") + + +@pytest.fixture(scope="function", params=return_template_files()) +def load_template_files(request): + """Return each *.textfsm file to run the syntax tests on.""" + return request.param + + +def test_template_compiles(load_template_files): + """Test that each template compiles with the textfsm parser. + + Compilation catches invalid rule indentation, references to undefined + states, a missing Start state, and duplicate state names. + """ + with open(load_template_files, encoding="utf-8") as fh: + textfsm.TextFSM(fh) + + +def test_rule_indentation(load_template_files): + """Test that every rule or comment line is indented with exactly 2 spaces.""" + bad_lines = [] + with open(load_template_files, encoding="utf-8") as fh: + for line_num, line in enumerate(fh, start=1): + match = RE_INDENTED_LINE.match(line) + if match and (match.group(1), match.group(2)) not in VALID_INDENTED_LINES: + bad_lines.append(line_num) + + assert not bad_lines, f"Rules must be indented with exactly 2 spaces, see line(s): {bad_lines}" + + +def test_no_unused_states(load_template_files): + """Test that every defined state is reachable from another rule.""" + with open(load_template_files, encoding="utf-8") as fh: + fsm = textfsm.TextFSM(fh) + + referenced_states = {rule.new_state for rules in fsm.states.values() for rule in rules} + unused_states = set(fsm.states) - IMPLICIT_STATES - referenced_states + + assert not unused_states, f"State(s) defined but never referenced by a rule: {sorted(unused_states)}" From 203e571b3f293b8616ea1a23131644637e3631fa Mon Sep 17 00:00:00 2001 From: Matt Vitale Date: Mon, 17 Aug 2026 16:32:30 -0400 Subject: [PATCH 2/3] Standardize indentation on 2 spaces --- .../arista_eos_show_ip_helper-address.textfsm | 16 +++---- .../templates/aruba_os_show_arp.textfsm | 16 +++---- ...eckpoint_gaia_show_arp_dynamic_all.textfsm | 4 +- ...sco_ios_show_crypto_session_detail.textfsm | 48 +++++++++---------- ...cisco_nxos_show_spanning-tree_root.textfsm | 2 +- .../cisco_nxos_show_switching-mode.textfsm | 2 +- ...eros_interface_lte_apn_print_terse.textfsm | 2 +- .../vmware_nsxv_show_ip_bgp_neighbors.textfsm | 10 ++-- .../vmware_nsxv_show_ip_route.textfsm | 2 +- 9 files changed, 51 insertions(+), 51 deletions(-) diff --git a/ntc_templates/templates/arista_eos_show_ip_helper-address.textfsm b/ntc_templates/templates/arista_eos_show_ip_helper-address.textfsm index cd52b8a23a..4bd8333d23 100644 --- a/ntc_templates/templates/arista_eos_show_ip_helper-address.textfsm +++ b/ntc_templates/templates/arista_eos_show_ip_helper-address.textfsm @@ -2,11 +2,11 @@ Value Required INTERFACE (\S+) Value List IP_HELPER (\d+\.\d+\.\d+\.\d+|\S+) Start - ^DHCP - ^Interface -> Continue.Record - ^Interface:\s+${INTERFACE}$$ - ^\s+DHCP\s+Smart - ^\s+DHCP\s+servers:\s+${IP_HELPER}$$ - ^\s+${IP_HELPER}$$ - ^$$ - ^. -> Error + ^DHCP + ^Interface -> Continue.Record + ^Interface:\s+${INTERFACE}$$ + ^\s+DHCP\s+Smart + ^\s+DHCP\s+servers:\s+${IP_HELPER}$$ + ^\s+${IP_HELPER}$$ + ^$$ + ^. -> Error diff --git a/ntc_templates/templates/aruba_os_show_arp.textfsm b/ntc_templates/templates/aruba_os_show_arp.textfsm index 1b102c3ff0..b15fea8fe7 100644 --- a/ntc_templates/templates/aruba_os_show_arp.textfsm +++ b/ntc_templates/templates/aruba_os_show_arp.textfsm @@ -5,11 +5,11 @@ Value INTERFACE (\S+) Value AGE (\d+) Start - ^Codes:\s+\*\s+-\s+Local\s+Addresses,\s+S\s+-\s+Static,\s+A\s+-\s+Auth - ^Total\s+ARP\s+entries:\s+\d+ - ^IPV4\s+ARP\s+Table - ^-+ - ^\s+Protocol\s+IP\s+Address\s+Hardware\s+Address\s+Interface\s+Age - ^\s+${PROTOCOL}\s+${IP_ADDRESS}\s+${MAC_ADDRESS}\s+${INTERFACE}\s+${AGE} -> Record - ^\s*$$ - ^. -> Error + ^Codes:\s+\*\s+-\s+Local\s+Addresses,\s+S\s+-\s+Static,\s+A\s+-\s+Auth + ^Total\s+ARP\s+entries:\s+\d+ + ^IPV4\s+ARP\s+Table + ^-+ + ^\s+Protocol\s+IP\s+Address\s+Hardware\s+Address\s+Interface\s+Age + ^\s+${PROTOCOL}\s+${IP_ADDRESS}\s+${MAC_ADDRESS}\s+${INTERFACE}\s+${AGE} -> Record + ^\s*$$ + ^. -> Error diff --git a/ntc_templates/templates/checkpoint_gaia_show_arp_dynamic_all.textfsm b/ntc_templates/templates/checkpoint_gaia_show_arp_dynamic_all.textfsm index 625e511e8e..3ac0c0f192 100644 --- a/ntc_templates/templates/checkpoint_gaia_show_arp_dynamic_all.textfsm +++ b/ntc_templates/templates/checkpoint_gaia_show_arp_dynamic_all.textfsm @@ -5,5 +5,5 @@ Start ^Dynamic\sArp\sParameters ^IP\sAddress\s+Mac\sAddress ^${IP_ADDRESS}\s+${MAC_ADDRESS} -> Record - ^\s*$$ - ^. -> Error + ^\s*$$ + ^. -> Error diff --git a/ntc_templates/templates/cisco_ios_show_crypto_session_detail.textfsm b/ntc_templates/templates/cisco_ios_show_crypto_session_detail.textfsm index 022aeb00b4..48d9738a2a 100644 --- a/ntc_templates/templates/cisco_ios_show_crypto_session_detail.textfsm +++ b/ntc_templates/templates/cisco_ios_show_crypto_session_detail.textfsm @@ -26,27 +26,27 @@ Value ACTIVE_SA (\d+) Value ORIGIN (.+) Start - ^Crypto\s+.* - ^Code: - ^K\s+- - ^X\s+- - ^R\s+- - ^S\s+- - ^Interface: -> Continue.Record - ^Interface:\s+${INTERFACE} - ^Profile:\s+${PROFILE} - ^Session\s+status:\s+${SESSION_STATUS} - ^Uptime:\s+${UPTIME} - ^Peer:\s+${PEER}\s+port\s+${PORT}\s+fvrf:\s+${FVRF}\s+ivrf:\s+${IVRF} - ^\s+Desc:\s+${DESCRIPTION} - ^\s+Phase1_id:\s+${PHASE1_ID} - ^\s+Session\s+ID:\s+${SESSION_ID} - ^\s+IKEv[1|2]\s+SA:\s+local\s+${LOCAL_IP}/${LOCAL_PORT}\s+remote\s+${REMOTE_IP}/${REMOTE_PORT}\s+${IKEV1_STATUS} - ^\s+Capabilities:${CAPABILITIES}\s+connid:${CONN_ID}\s+lifetime:${LIFETIME} - ^\s+IPSEC\s+FLOW:\s+permit\s+${PERMIT}\s+host\s+${SRC_HOST}\s+host\s+${DST_HOST} - ^\s+IPSEC\s+FLOW:\s+permit\s+${PERMIT}\s+${SRC_HOST}/${SRC_MASK}\s+${DST_HOST}/${DST_MASK} - ^\s+Active\s+SAs:\s+${ACTIVE_SA},\s+origin:\s+${ORIGIN} - ^\s+Inbound:\s+#.* - ^\s+Outbound:\s+#.* - ^\s*$$ - ^. -> Error + ^Crypto\s+.* + ^Code: + ^K\s+- + ^X\s+- + ^R\s+- + ^S\s+- + ^Interface: -> Continue.Record + ^Interface:\s+${INTERFACE} + ^Profile:\s+${PROFILE} + ^Session\s+status:\s+${SESSION_STATUS} + ^Uptime:\s+${UPTIME} + ^Peer:\s+${PEER}\s+port\s+${PORT}\s+fvrf:\s+${FVRF}\s+ivrf:\s+${IVRF} + ^\s+Desc:\s+${DESCRIPTION} + ^\s+Phase1_id:\s+${PHASE1_ID} + ^\s+Session\s+ID:\s+${SESSION_ID} + ^\s+IKEv[1|2]\s+SA:\s+local\s+${LOCAL_IP}/${LOCAL_PORT}\s+remote\s+${REMOTE_IP}/${REMOTE_PORT}\s+${IKEV1_STATUS} + ^\s+Capabilities:${CAPABILITIES}\s+connid:${CONN_ID}\s+lifetime:${LIFETIME} + ^\s+IPSEC\s+FLOW:\s+permit\s+${PERMIT}\s+host\s+${SRC_HOST}\s+host\s+${DST_HOST} + ^\s+IPSEC\s+FLOW:\s+permit\s+${PERMIT}\s+${SRC_HOST}/${SRC_MASK}\s+${DST_HOST}/${DST_MASK} + ^\s+Active\s+SAs:\s+${ACTIVE_SA},\s+origin:\s+${ORIGIN} + ^\s+Inbound:\s+#.* + ^\s+Outbound:\s+#.* + ^\s*$$ + ^. -> Error diff --git a/ntc_templates/templates/cisco_nxos_show_spanning-tree_root.textfsm b/ntc_templates/templates/cisco_nxos_show_spanning-tree_root.textfsm index b9f8890e9f..3e297ce5f0 100644 --- a/ntc_templates/templates/cisco_nxos_show_spanning-tree_root.textfsm +++ b/ntc_templates/templates/cisco_nxos_show_spanning-tree_root.textfsm @@ -12,4 +12,4 @@ Start ^\s*Vlan\s*Root\sID\s*Cost\s*Time\s*Age\s*Dly\s*Root\s*Port$$ ^\s*- ^\s*${VLAN_ID}\s+${PRIORITY}\s+${ROOT_ID}+\s+${ROOT_COST}+\s+${HELLO_TIME}+\s+${MAX_AGE}+\s+${FWD_DLY}+\s+${ROOT_PORT} -> Record - ^. -> Error \ No newline at end of file + ^. -> Error \ No newline at end of file diff --git a/ntc_templates/templates/cisco_nxos_show_switching-mode.textfsm b/ntc_templates/templates/cisco_nxos_show_switching-mode.textfsm index 85eaa4ff39..a39b96bf17 100644 --- a/ntc_templates/templates/cisco_nxos_show_switching-mode.textfsm +++ b/ntc_templates/templates/cisco_nxos_show_switching-mode.textfsm @@ -7,4 +7,4 @@ Start ^\s*Module\s*Number\s*Operational\s*Mode\s*$$ ^\s*${MODULE_NUMBER}\s*${OPERATIONAL_MODE}\s*$$ -> Record ^\s*$$ - ^. -> Error \ No newline at end of file + ^. -> Error \ No newline at end of file diff --git a/ntc_templates/templates/mikrotik_routeros_interface_lte_apn_print_terse.textfsm b/ntc_templates/templates/mikrotik_routeros_interface_lte_apn_print_terse.textfsm index dcf9d4208c..b37aba5669 100644 --- a/ntc_templates/templates/mikrotik_routeros_interface_lte_apn_print_terse.textfsm +++ b/ntc_templates/templates/mikrotik_routeros_interface_lte_apn_print_terse.textfsm @@ -10,6 +10,6 @@ Value DEFAULT_ROUTE_DISTANCE (\d+) Value AUTHENTICATION (\S+) Start - ^${INDEX}\s+(${FLAGS}\s+)?name=${NAME}\s+apn=${APN}\s+use-peer-dns=${USE_PEER_DNS}\s+use-network-apn=${USE_NETWORK_APN}\s+add-default-route=${ADD_DEFAULT_ROUTE}\s+default-route-distance=${DEFAULT_ROUTE_DISTANCE}\s+ip-type=${IP_TYPE}\s+authentication=${AUTHENTICATION}\s*$$ + ^${INDEX}\s+(${FLAGS}\s+)?name=${NAME}\s+apn=${APN}\s+use-peer-dns=${USE_PEER_DNS}\s+use-network-apn=${USE_NETWORK_APN}\s+add-default-route=${ADD_DEFAULT_ROUTE}\s+default-route-distance=${DEFAULT_ROUTE_DISTANCE}\s+ip-type=${IP_TYPE}\s+authentication=${AUTHENTICATION}\s*$$ ^\s*$$ ^. -> Error diff --git a/ntc_templates/templates/vmware_nsxv_show_ip_bgp_neighbors.textfsm b/ntc_templates/templates/vmware_nsxv_show_ip_bgp_neighbors.textfsm index 5831c3900a..69316dc639 100644 --- a/ntc_templates/templates/vmware_nsxv_show_ip_bgp_neighbors.textfsm +++ b/ntc_templates/templates/vmware_nsxv_show_ip_bgp_neighbors.textfsm @@ -8,8 +8,8 @@ Value PFX_SENT (\d+) Value PFX_ADV (\d+) Start - ^BGP neighbor.* -> Continue.Record - ^BGP neighbor is ${NEIGHBOR_ID},\s+remote AS ${REMOTE_AS} - ^BGP state = ${STATE} - ^Hold time is ${HOLD_INTERVAL}, Keep alive interval is ${KEEPALIVE_INTERVAL} - ^\s+Prefixes received ${PFX_RECV} sent ${PFX_SENT} advertised ${PFX_ADV} \ No newline at end of file + ^BGP neighbor.* -> Continue.Record + ^BGP neighbor is ${NEIGHBOR_ID},\s+remote AS ${REMOTE_AS} + ^BGP state = ${STATE} + ^Hold time is ${HOLD_INTERVAL}, Keep alive interval is ${KEEPALIVE_INTERVAL} + ^\s+Prefixes received ${PFX_RECV} sent ${PFX_SENT} advertised ${PFX_ADV} \ No newline at end of file diff --git a/ntc_templates/templates/vmware_nsxv_show_ip_route.textfsm b/ntc_templates/templates/vmware_nsxv_show_ip_route.textfsm index 5ba5a83d39..6f973cf607 100644 --- a/ntc_templates/templates/vmware_nsxv_show_ip_route.textfsm +++ b/ntc_templates/templates/vmware_nsxv_show_ip_route.textfsm @@ -7,4 +7,4 @@ Value METRIC (\d+) Value NEXTHOP (\d+(\.\d+){3}) Start - ^${PROTOCOL}\s+${TYPE}\s+${IP_ADDRESS}\/${PREFIX_LENGTH}\s+\[${DISTANCE}\/${METRIC}\]\s+via\s+${NEXTHOP} -> Next.Record + ^${PROTOCOL}\s+${TYPE}\s+${IP_ADDRESS}\/${PREFIX_LENGTH}\s+\[${DISTANCE}\/${METRIC}\]\s+via\s+${NEXTHOP} -> Next.Record From 98408ff14880426254e79605b85f7358220f48bc Mon Sep 17 00:00:00 2001 From: Matt Vitale Date: Mon, 17 Aug 2026 16:36:04 -0400 Subject: [PATCH 3/3] Remove unreachable Done states --- ntc_templates/templates/avaya_ers_show_interface_name.textfsm | 2 -- .../templates/avaya_ers_show_mac-address-table.textfsm | 2 -- ntc_templates/templates/avaya_vsp_show_software.textfsm | 2 -- .../templates/cisco_nxos_show_ip_bgp_summary_vrf.textfsm | 3 --- ntc_templates/templates/ruckus_fastiron_show_vlan.textfsm | 3 --- 5 files changed, 12 deletions(-) diff --git a/ntc_templates/templates/avaya_ers_show_interface_name.textfsm b/ntc_templates/templates/avaya_ers_show_interface_name.textfsm index 3097d1f979..adce5c2a35 100644 --- a/ntc_templates/templates/avaya_ers_show_interface_name.textfsm +++ b/ntc_templates/templates/avaya_ers_show_interface_name.textfsm @@ -5,5 +5,3 @@ Start ^Port Name|Unit\/Port Name ^-+ -+ ^${PORT}\s+${NAME} -> Record - -Done diff --git a/ntc_templates/templates/avaya_ers_show_mac-address-table.textfsm b/ntc_templates/templates/avaya_ers_show_mac-address-table.textfsm index b9d235b4fd..089d486c26 100644 --- a/ntc_templates/templates/avaya_ers_show_mac-address-table.textfsm +++ b/ntc_templates/templates/avaya_ers_show_mac-address-table.textfsm @@ -7,5 +7,3 @@ Start ^\s+MAC Address.* ^-+ -+ -+ -+ ^${MAC_ADDRESS}\s+${VID}\s+${TYPE}\s+\w*:*\s*${PORT}* -> Record - -Done \ No newline at end of file diff --git a/ntc_templates/templates/avaya_vsp_show_software.textfsm b/ntc_templates/templates/avaya_vsp_show_software.textfsm index e3c180213a..8fcacaec84 100644 --- a/ntc_templates/templates/avaya_vsp_show_software.textfsm +++ b/ntc_templates/templates/avaya_vsp_show_software.textfsm @@ -11,5 +11,3 @@ Start ^${PRIMARY_RELEASE}\s+\(Primary Release\) ^Auto Commit\s+:\s+${AUTO_COMMIT} ^Commit Timeout\s+:\s+${COMMIT_TIMEOUT} - -Done diff --git a/ntc_templates/templates/cisco_nxos_show_ip_bgp_summary_vrf.textfsm b/ntc_templates/templates/cisco_nxos_show_ip_bgp_summary_vrf.textfsm index cb33dbb66f..ba0fb2b955 100644 --- a/ntc_templates/templates/cisco_nxos_show_ip_bgp_summary_vrf.textfsm +++ b/ntc_templates/templates/cisco_nxos_show_ip_bgp_summary_vrf.textfsm @@ -24,6 +24,3 @@ Start ^${BGP_NEIGH}\s+${BGP_VER}\s+${NEIGH_AS}\s+${MSG_RCVD}\s+${MSG_SENT}\s+${TBLVER}\s+${IN_QUEUE}\s+${OUT_QUEUE}\s+${UP_DOWN}\s+${STATE_PFXRCD}\s*$$ -> Record # Match lines that are spaces ^\s+$$ - -Done - ^.* diff --git a/ntc_templates/templates/ruckus_fastiron_show_vlan.textfsm b/ntc_templates/templates/ruckus_fastiron_show_vlan.textfsm index 8b3b7b1233..94131d5a95 100644 --- a/ntc_templates/templates/ruckus_fastiron_show_vlan.textfsm +++ b/ntc_templates/templates/ruckus_fastiron_show_vlan.textfsm @@ -41,6 +41,3 @@ Start ^\s+Monitoring:\s+ ^\s*$$ ^.+ -> Error - -Done - ^.* \ No newline at end of file