Skip to content
LinkState
Go back

The linter failures worth surfacing to operators

Introduction to AI‑Generated Configs Validation

AI‑generated network configurations often fail CI pipelines with a generic “rejected by pipeline” banner. Engineers need concrete artifacts to triage failures:

Each artifact isolates a class of error, turning a black‑box rejection into an actionable checklist.

Why Detailed Validation Feedback Matters

Detailed feedback reduces mean‑time‑to‑repair (MTTR) by shifting work from guesswork to targeted fixes.

Together, these artifacts form a validation stack that moves beyond syntactic linting to semantic correctness.


Parser Offsets

Definition

A parser offset is the byte or character index where a configuration parser reports a syntax error. Unlike a generic “invalid config” message, the offset tells the engineer exactly where the token stream diverges from the grammar. Offsets are emitted by vendors’ CLI parsers (e.g., Cisco IOS‑XR, Juniper Junos) or by open‑source parsers such as pyang for YANG or configtree for vendor‑agnostic models. The offset enables line‑number mapping, syntax‑highlighting in editors, and automated patch generation.

Typical Use Cases

Example: Extracting Offsets with ciscoconfparse

from ciscoconfparse import CiscoConfParse
import re
import sys

def find_parse_offset(cfg_path):
    try:
        CiscoConfParse(cfg_path, syntax='ios')
        return None  # No error found
    except Exception as e:
        msg = str(e)
        m = re.search(r'line (\d+):', msg)
        if m:
            line_no = int(m.group(1))
            with open(cfg_path, 'r', encoding='utf-8') as f:
                data = f.read()
                lines = data.splitlines(True)
                offset = sum(len(l) for l in lines[:line_no-1])
                return offset, line_no, msg
        return None, None, msg

if __name__ == '__main__':
    offset, line, err = find_parse_offset('generated.cfg')
    if offset is None:
        print('Config parsed successfully')
    else:
        print(f'Parse error at byte offset ~{offset} (line {line}): {err}')

Notice:


Unsupported Knobs

Identification

Unsupported knobs are configuration statements that the target network operating system (NOS) does not recognize. AI models trained on mixed‑vendor corpora may emit Juniper‑style set protocols mpls label-switched-path on an IOS‑XR device, or vice‑versa. Detection relies on comparing the rendered config against the NOS feature model (often a YANG schema or vendor‑specific CLI grammar). Tools such as Batfish’s question.parse_config() or custom ansible-lint rules can flag unknown tokens.

Troubleshooting Workflow

  1. Remove the knob if it is non‑essential.
  2. Replace it with an equivalent supported knob (e.g., bgp graceful-restartbgp graceful-restart stalepath-time).
  3. Conditionally exclude it via Jinja2 templating based on an inventory variable that defines the NOS family.

Steps:

CLI Example: Detecting Unsupported Knobs with Batfish

# Assume a Batfish service is running and a snapshot named 'lab' exists
bf session set snapshot lab

# Parse the config and collect unsupported lines
bf question.parse_config \
    --nodes 'all' \
    --output-format json \
| jq '.[] | select(.unsupported == true) | .line, .text'

Sample output (annotated):

{
  "line": 58,
  "text": "set protocols mpls label-switched-path LSP1 to 10.0.0.2",
  "unsupported": true,
  "reason": "Command not found in IOS‑XR grammar"
}

Notice:


Undefined Variables

Causes and Consequences

Undefined variables arise when a Jinja2 (or similar) template references a variable not supplied in the rendering context. Common causes:

Consequences include template rendering aborts, empty strings that produce invalid CLI (e.g., neighbor remote-as), or silent misconfiguration if the variable defaults to an empty string and the resulting command is accepted but nonsensical.

Handling Strategies

Example: Rendering with Strict Undefined Handling

from jinja2 import Environment, StrictUndefined, FileSystemLoader
import os

def render_template(template_path, context):
    env = Environment(
        loader=FileSystemLoader(searchpath=os.path.dirname(template_path)),
        undefined=StrictUndefined,
        trim_blocks=True,
        lstrip_blocks=True,
    )
    template = env.get_template(os.path.basename(template_path))
    try:
        rendered = template.render(**context)
        return rendered, None
    except Exception as e:
        return None, str(e)

if __name__ == '__main__':
    tmpl = 'templates/bgp.j2'
    ctx = {
        'local_as': 65001,
        # 'peer_as' intentionally omitted to trigger error
    }
    rendered, err = render_template(tmpl, ctx)
    if err:
        print(f'Undefined variable error: {err}')
        # Expected output: "UndefinedError: 'peer_as' is undefined"
    else:
        print(rendered)

Notice:


Expansion Diffs

What They Show

Expansion diffs display the difference between a template (with placeholders, loops, conditionals) and its fully expanded rendering after variable substitution and macro expansion. Unlike a plain diff between two static files, expansion diffs highlight what the AI‑generated template actually produces, making it easy to spot unintended side‑effects such as duplicated statements, over‑broad allow-service clauses, or missing no negations.

Review Process

  1. Render the template with the intended context → expanded.cfg.
  2. Generate a diff between the template file and expanded.cfg (e.g., diff -u or git diff --no-index).
  3. Examine added lines: these are the concrete configuration bits that will be pushed to devices.
  4. Look for patterns:
    • Blocks that appear many times → possible over‑generation.
    • Missing no prefixes on features that should be disabled.
    • Hard‑coded values that should be parameterized.
      If the diff reveals surprises, adjust the template or context and re‑render.

Example: Misplaced Command Outside a Loop

Template (interface.j2):

interface GigabitEthernet0/0
{% for vlan in vlans %}
  GigabitEthernet0/0.{{ vlan }}
{% endfor %}
 encapsulation dot1q  {{ vlan }}   <-- BUG: vlan undefined here

Context: {"vlans": [10,20,30]}

Rendered config (expanded.cfg):

interface GigabitEthernet0/0
  GigabitEthernet0/0.10
  GigabitEthernet0/0.20
  GigabitEthernet0/0.30
 encapsulation dot1q  10

Diff (diff -u interface.j2 expanded.cfg):

--- interface.j2
+++ expanded.cfg
@@
 interface GigabitEthernet0/0
+  GigabitEthernet0/0.10
+  GigabitEthernet0/0.20
+  GigabitEthernet0/0.30
+ encapsulation dot1q  10

Notice:


Failed Lab Tests

Role of Lab Tests

Lab tests instantiate a simulated topology (using Containerlab, vrnetlab, GNS3, etc.) and apply the rendered config to virtual routers. They then run protocol‑level checks (e.g., BGP session state, OSPF adjacency, ping sweep) to verify that the config achieves the intended intent. A failed lab test indicates that the config, while syntactically valid, does not produce the expected operational state.

Analyzing a Failure

Collect:

Proceed from the symptom (e.g., no BGP peers) to the root cause (e.g., missing update-source or incorrect AS number), adjust the template or variables, and re‑run the validation stack.


By leveraging parser offsets, unsupported‑knob lists, undefined‑variable reports, expansion diffs, and failed lab test results, engineers can move from a vague “rejected by pipeline” banner to a precise, actionable debugging workflow. This reduces MTTR and increases confidence that AI‑generated configs are both syntactically correct and semantically sound.


Share this post on:

Previous Post
How netem reorder corrupts your TCP benchmark story
Next Post
NAT can make ACL unit tests lie