<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Network Plumber]]></title><description><![CDATA[Lab-to-production walkthroughs for engineers who learn by building.]]></description><link>https://www.byrnbaker.me</link><image><url>https://substackcdn.com/image/fetch/$s_!R3Vw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F50d55865-99d0-4872-8120-1f3384dd2766_588x588.png</url><title>Network Plumber</title><link>https://www.byrnbaker.me</link></image><generator>Substack</generator><lastBuildDate>Tue, 25 Aug 2026 09:14:22 GMT</lastBuildDate><atom:link href="https://www.byrnbaker.me/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Byrn Baker]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[networkplumber@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[networkplumber@substack.com]]></itunes:email><itunes:name><![CDATA[Byrn Baker]]></itunes:name></itunes:owner><itunes:author><![CDATA[Byrn Baker]]></itunes:author><googleplay:owner><![CDATA[networkplumber@substack.com]]></googleplay:owner><googleplay:email><![CDATA[networkplumber@substack.com]]></googleplay:email><googleplay:author><![CDATA[Byrn Baker]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[13 Cisco Routers. Zero Manual Configuration]]></title><description><![CDATA[How I used Nautobot Golden Config to deploy and verify a complete MPLS VPN control plane.]]></description><link>https://www.byrnbaker.me/p/13-cisco-routers-zero-manual-configuration</link><guid isPermaLink="false">https://www.byrnbaker.me/p/13-cisco-routers-zero-manual-configuration</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 20 Aug 2026 13:02:42 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8fcff0c7-ba92-473c-beb6-ad84bc650008_1168x784.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hyxg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hyxg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hyxg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg" width="1168" height="784" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:784,&quot;width&quot;:1168,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:277271,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/211482753?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hyxg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!hyxg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd91a69-0ca2-4a90-bc20-7a716df5a3da_1168x784.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><blockquote><p><strong>TLDR:</strong> Part 3 of 10. We configure Nautobot credentials, Nornir, compliance features, and Golden Config Plans, then deploy generated intent to 13 Cisco IOS-XE routers in CML. We finish by verifying IS-IS, MPLS LDP, VPNv4 and VPNv6 route reflection, and PE-to-CE BGP.</p></blockquote><h2><span>Where we left off</span></h2><p>Part 2 rendered intended configurations for the full topology from Nautobot Source of Truth data. This walkthrough narrows the deployment to the 13 Cisco routers that make up the service-provider network:</p><ul><li><p>Four P routers running IS-IS and MPLS</p></li><li><p>Two VPNv4 and VPNv6 route reflectors</p></li><li><p>One border router</p></li><li><p>Three service-provider edge routers</p></li><li><p>Three customer edge routers</p></li></ul><p>The intended files are stored in Git, but nothing should reach a router until we can answer four questions:</p><ol><li><p>Can the worker authenticate to every device?</p></li><li><p>Does compliance extract the right configuration sections?</p></li><li><p>Will those sections be sent in a dependency-safe order?</p></li><li><p>Can we prove that running configuration matches intent after deployment?</p></li></ol><p>This walkthrough sets up each layer in that order.</p><h2><span>Deployment workflow</span></h2><p>Golden Config deployment depends on fresh output from every earlier stage:</p><pre><code><code>Credentials + primary IP + network reachability
    -&gt; intended configuration
    -&gt; running-config backup
    -&gt; compliance rules
    -&gt; compliance results
    -&gt; Config Plan
    -&gt; review and approval
    -&gt; deployment
    -&gt; new backup and compliance run</code></code></pre><p>A Config Plan stores command text when it is created. If intent, backups, rules, or source-of-truth data change, generate a new plan.</p>
      <p>
          <a href="https://www.byrnbaker.me/p/13-cisco-routers-zero-manual-configuration">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Catch Broken Network Configs Before They Reach a Router]]></title><description><![CDATA[TLDR: Part 3 deploys generated intent to 13 Cisco IOS-XE routers on Thursday.]]></description><link>https://www.byrnbaker.me/p/catch-broken-network-configs-before</link><guid isPermaLink="false">https://www.byrnbaker.me/p/catch-broken-network-configs-before</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Wed, 19 Aug 2026 13:03:29 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0103085e-1e7c-4b15-a9cb-533ea116b3f1_1960x1283.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><strong>TLDR:</strong> Part 3 deploys generated intent to 13 Cisco IOS-XE routers on Thursday. Before those configs reach a device, we add three validation gates: Jinja2 linting, strict render tests, and Batfish parsing. Together they catch template syntax errors, missing data, and invalid IOS-XE commands in under five seconds.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.byrnbaker.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Network Plumber is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2><span>The problem we&#8217;re solving</span></h2><p>Part 3 deploys generated intent to 13 Cisco IOS-XE routers this Thursday. While building it, I found a gap we needed to close first: a rendered configuration is not necessarily a valid configuration.</p><p>Parts 1 and 2 got us from Nautobot source-of-truth data to rendered config files. When I started working through the deployment, every template change followed the same pattern:</p><ol><li><p>Edit the Jinja2 template</p></li><li><p>Regenerate intended configs</p></li><li><p>Build a Config Plan</p></li><li><p>Deploy to one device</p></li><li><p>Watch it fail with <code>% Invalid input detected</code></p></li><li><p>Read the error, fix the template</p></li><li><p>Go back to step 2</p></li></ol><p>That is the network version of testing in production. Each iteration costs 5 to 10 minutes while jobs run, configs render, and plans build. A missing <code>| default()</code> filter or a typo in a command can burn 30 minutes before you find it.</p><p>Software teams solved this problem decades ago. You don&#8217;t deploy code to production to find out if it compiles. You run the build locally, run the tests, and only ship what passes. We&#8217;re going to do the same thing for network configuration.</p><h2><span>What we&#8217;re building</span></h2><p>Three validation gates, each catching a different class of error:</p><pre><code><code>Template change
    -&gt; j2lint (Jinja syntax: unclosed blocks, bad delimiters)
    -&gt; pytest render (undefined variables, None in output, empty config)
    -&gt; Batfish parse (invalid CLI commands, malformed syntax)
    -&gt; commit allowed
    -&gt; push triggers GitHub Actions (same checks, fresh environment)
    -&gt; only then: regenerate intent, compliance, deploy</code></code></pre><p>The fast path (<code>make ci</code>) takes under 2 seconds. The full path with Batfish (<code>make ci-full</code>) adds about 3 more. Either way, it&#8217;s faster than a single Nautobot job run.</p><h2><span>Layer 1: Jinja2 linting with j2lint</span></h2><p><a href="https://github.com/aristanetworks/j2lint">j2lint</a> comes from Arista&#8217;s AVD team. It checks Jinja2 template syntax without needing any context data. Think of it as a compiler for your templates.</p><pre><code><code>pip install j2lint</code></code></pre><p>Run it against the templates directory:</p><pre><code><code>j2lint golden-config/templates --extensions j2 \
  -i jinja-statements-indentation single-statement-per-line</code></code></pre><p>We ignore two rules. The <code>jinja-statements-indentation</code> rule (S4) wants Jinja control blocks indented by 4 spaces per nesting level. In network config templates, that would mean <code>{% if %}</code> blocks indented 20+ spaces deep while the IOS commands they produce sit at 1-space indent. It makes the templates unreadable. The <code>single-statement-per-line</code> rule flags <code>{% if list.append(x) %}{% endif %}</code>, which is a standard Jinja idiom for building lists during iteration.</p><p>Everything else stays on. If you forget to close a <code>{% for %}</code> block or misspell a filter name, j2lint catches it instantly.</p><h2><span>Layer 2: Render tests with strict variable checking</span></h2><p>This is where the real value lives. We render every template against mock SoT data using Jinja2&#8217;s <code>StrictUndefined</code> mode. If the template references a variable that doesn&#8217;t exist in the context, the test explodes with the exact line and variable name.</p><h3><span>The mock contexts</span></h3><p>We need representative data for each device role and platform combination. I pulled these directly from the Nautobot GraphQL query that golden config uses:</p><pre><code><code>tests/mock_contexts/
&#9500;&#9472;&#9472; cisco_ios_route_reflector.yaml   # RR1: ISIS, MPLS, BGP with RR-client logic
&#9500;&#9472;&#9472; cisco_ios_pe_router.yaml         # SPE1: VRF, PE-CE eBGP, full SP stack
&#9500;&#9472;&#9472; arista_eos_leaf.yaml             # DCA-Leaf01: EVPN, VXLAN, SVIs, VRFs
&#9492;&#9472;&#9472; arista_eos_spine.yaml            # DCA-Spine01: underlay + overlay redistribution
</code></code></pre><p>Each file mirrors the exact shape of what the <code>sp_demo_lab_golden_config</code> GraphQL query returns. Here&#8217;s a trimmed example for the PE router:</p><pre><code><code>hostname: SPE1

config_context:
  isis:
    process_name: SP-ISIS
    metric_style: wide
    is_type: level-2-only
  mpls:
    ldp_router_id: Loopback0
    ldp_sync: true
    explicit_null: true
  bgp:
    timers:
      keepalive: 10
      hold: 30
    default_ipv4_unicast: false

interfaces:
  - name: GigabitEthernet3
    description: "to CE1 GigabitEthernet2"
    enabled: true
    vrf:
      name: CUSTOMER-A
      rd: "65000:100"
    ip_addresses:
      - address: "172.16.0.0/31"
        ip_version: 4
    # ... full structure continues</code></code></pre><h3><span>The test file</span></h3><pre><code><code># tests/test_template_render.py
import jinja2
import pytest
import yaml

DEVICE_SCENARIOS = [
    ("cisco_ios_route_reflector.yaml", "cisco_ios"),
    ("cisco_ios_pe_router.yaml", "cisco_ios"),
    ("arista_eos_leaf.yaml", "arista_eos"),
    ("arista_eos_spine.yaml", "arista_eos"),
]

def build_jinja_env():
    return jinja2.Environment(
        loader=jinja2.FileSystemLoader(str(REPO_ROOT)),
        undefined=jinja2.StrictUndefined,  # THIS IS THE KEY
        trim_blocks=True,
        keep_trailing_newline=True,
    )

@pytest.mark.parametrize("context_file,platform", DEVICE_SCENARIOS)
def test_template_renders_without_error(context_file, platform):
    env = build_jinja_env()
    template = env.get_template(f"golden-config/templates/{platform}.j2")
    context = load_context(context_file)
    rendered = template.render(**context)

    assert len(rendered.strip()) &gt; 50</code></code></pre><p>The <code>StrictUndefined</code> setting is the entire point. Without it, Jinja2 silently renders missing variables as empty strings. With it, you get:</p><pre><code><code>jinja2.exceptions.UndefinedError: 'config_context' is undefined
  File "golden-config/templates/ios/isis.j2", line 3
</code></code></pre><p>That error message tells you exactly what&#8217;s wrong and where. Compare that to deploying and getting <code>% Invalid input detected at '^' marker</code> from the device, which tells you nothing about the root cause.</p><h3><span>What else the tests check</span></h3><p>Beyond <code>StrictUndefined</code>, we validate four things per scenario:</p><ol><li><p>The rendered output is longer than 50 characters (catches templates that render empty)</p></li><li><p>No raw <code>{{</code> or <code>{%</code> tags leak into the output (catches templates that partially fail)</p></li><li><p>No Python <code>None</code> appears in the config lines (catches missing <code>| default()</code> filters)</p></li><li><p>The first meaningful line is <code>hostname &lt;expected&gt;</code> (catches structural problems)</p></li></ol><h2><span>Layer 3: Batfish vendor-aware config parsing</span></h2><p>Batfish parses network configs the way a router does. It builds a vendor-specific model of your configuration and flags anything the device parser would reject.</p><h3><span>Setting it up</span></h3><pre><code><code>docker run -d --name batfish -p 9997:9997 -p 9996:9996 batfish/batfish:latest
pip install pybatfish</code></code></pre><h3><span>What it validates</span></h3><p>We render the templates, write them as <code>.cfg</code> files to a temp directory, and feed them to Batfish as a &#8220;snapshot.&#8221; Batfish then parses each file and reports:</p><ul><li><p>Parse status (PASSED, PARTIALLY_UNRECOGNIZED, or FAILED)</p></li><li><p>Parse warnings (specific lines it couldn&#8217;t understand)</p></li><li><p>Undefined references (route-maps, ACLs, or prefix-lists that are referenced but never defined)</p></li></ul><p>For our IOS-XE devices, Batfish reported zero unexpected warnings. The only flagged line was <code>ip ssh bulk-mode 131072</code>, which is a newer IOS-XE 17.x command that Batfish&#8217;s grammar hasn&#8217;t added yet. We mark that as known-benign.</p><pre><code><code>KNOWN_BENIGN_IOS = {
    "ip ssh bulk-mode",  # IOS-XE 17.x feature, Batfish grammar is behind
}</code></code></pre><p>If you introduced a typo like <code>routr bgp 65000</code> or <code>ip addres 10.0.0.1 255.255.255.0</code>, Batfish would catch it here. No device touched.</p><h3><span>The EOS caveat</span></h3><p>Batfish currently misidentifies Arista EOS configs as Cisco IOS, which produces many false-positive warnings for EOS-specific syntax (<code>vrf instance</code>, <code>neighbor X peer group</code>, VXLAN commands). We filter these out and rely primarily on the Jinja render tests for EOS validation. As Batfish improves its EOS parser detection, this will get better.</p><h3><span>Cross-device BGP analysis</span></h3><p>The more interesting Batfish capability is cross-device validation. When you load configs for multiple devices, Batfish can verify that BGP sessions have matching configurations on both sides:</p><pre><code><code>def test_bgp_session_compatibility(self, batfish_full_results):
    bf = batfish_full_results
    bgp_edges = bf.q.bgpEdges().answer().frame()
    # If edges exist, the sessions are configured consistently</code></code></pre><p>And it can find undefined references (a route-map referenced in a neighbor statement that doesn&#8217;t exist anywhere):</p><pre><code><code>def test_undefined_references(self, batfish_full_results):
    bf = batfish_full_results
    undef = bf.q.undefinedReferences().answer().frame()
    # These would cause silent policy failures on the device</code></code></pre><h2><span>Running it locally</span></h2><p>The Makefile gives you three entry points:</p><pre><code><code>make ci        # j2lint + render tests (~2 seconds, no container needed)
make ci-full   # above + Batfish validation (~5 seconds, needs container)
make validate  # Batfish only</code></code></pre><p>Here&#8217;s what a clean run looks like:</p><pre><code><code>$ make ci-full
j2lint golden-config/templates --extensions j2 -i jinja-statements-indentation single-statement-per-line
pytest tests/test_template_render.py -v
tests/test_template_render.py::test_template_renders_without_error[cisco_ios_route_reflector.yaml-cisco_ios] PASSED
tests/test_template_render.py::test_template_renders_without_error[cisco_ios_pe_router.yaml-cisco_ios] PASSED
tests/test_template_render.py::test_template_renders_without_error[arista_eos_leaf.yaml-arista_eos] PASSED
tests/test_template_render.py::test_template_renders_without_error[arista_eos_spine.yaml-arista_eos] PASSED
...
16 passed in 0.93s

pytest tests/test_batfish_validate.py -v
tests/test_batfish_validate.py::TestBatfishIOSValidation::test_all_ios_configs_parsed PASSED
tests/test_batfish_validate.py::TestBatfishIOSValidation::test_no_unexpected_ios_parse_warnings PASSED
tests/test_batfish_validate.py::TestBatfishEOSValidation::test_all_eos_configs_parsed PASSED
tests/test_batfish_validate.py::TestBatfishEOSValidation::test_no_unexpected_eos_parse_warnings PASSED
tests/test_batfish_validate.py::TestBatfishBGPValidation::test_bgp_session_compatibility PASSED
tests/test_batfish_validate.py::TestBatfishBGPValidation::test_undefined_references PASSED
6 passed in 2.78s</code></code></pre><p>22 tests, under 4 seconds, zero devices involved.</p><h2><span>The pre-commit hook</span></h2><p>We don&#8217;t want to rely on remembering to run <code>make ci</code>. A git pre-commit hook runs it automatically whenever you commit files that touch templates or config contexts:</p><pre><code><code>#!/bin/bash
# .git/hooks/pre-commit

STAGED_TEMPLATES=$(git diff --cached --name-only | \
  grep -E '(golden-config/templates/|config_contexts/|tests/mock_contexts/)' || true)

if [ -z "$STAGED_TEMPLATES" ]; then
    exit 0
fi

echo "Template files staged for commit, running validation..."
make ci</code></code></pre><p>If validation fails, the commit is rejected. You can bypass with <code>--no-verify</code>, but you shouldn&#8217;t.</p><h2><span>GitHub Actions for CI/CD</span></h2><p>The pre-commit hook is a local safety net. For the team-level guarantee, we add a GitHub Actions workflow that triggers on pushes and PRs touching template files:</p><pre><code><code># .github/workflows/validate-templates.yml
name: Validate Golden Config Templates

on:
  push:
    paths:
      - 'golden-config/templates/**'
      - 'config_contexts/**'
      - 'tests/**'
  pull_request:
    paths:
      - 'golden-config/templates/**'
      - 'config_contexts/**'
      - 'tests/**'

jobs:
  lint-and-render:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install jinja2 pyyaml pytest j2lint
      - run: make ci

  batfish-validate:
    runs-on: ubuntu-latest
    needs: lint-and-render
    services:
      batfish:
        image: batfish/batfish:latest
        ports:
          - 9997:9997
          - 9996:9996
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install jinja2 pyyaml pytest pybatfish
      - run: make validate</code></code></pre><p>The pipeline is two stages. The fast lint-and-render job runs first. If it fails, Batfish never spins up. If it passes, the Batfish service container starts and runs the deeper validation. GitHub Actions provides the container as a service, so there&#8217;s no Docker-in-Docker complexity.</p><h2><span>The maintenance trade-off</span></h2><p>The mock contexts need updating when you add new variables to templates. That&#8217;s the cost. If you add <code>config_context.new_feature.key</code> to a template but don&#8217;t add it to the mock, pytest catches it immediately:</p><pre><code><code>jinja2.exceptions.UndefinedError: 'dict object' has no attribute 'new_feature'</code></code></pre><p>This is a feature, not a bug. It forces you to answer &#8220;where does this data come from?&#8221; before the template consumes it. Is it in the config context YAML? Is it in the GraphQL query? If neither, the variable will fail in production too, not just in the test.</p><h2><span>The full workflow now</span></h2><pre><code><code>1. Edit template or config context
2. git add + git commit
   &#9492;&#9472;&#9472; pre-commit hook runs `make ci`
       &#9500;&#9472;&#9472; j2lint: Jinja syntax OK?
       &#9492;&#9472;&#9472; pytest: renders clean with all variables?
3. git push
   &#9492;&#9472;&#9472; GitHub Actions triggers
       &#9500;&#9472;&#9472; lint-and-render job (same as local)
       &#9492;&#9472;&#9472; batfish-validate job (vendor grammar check)
4. CI passes &#8594; regenerate intended in Nautobot
5. Nautobot pushes intended configs to Git
   &#9492;&#9472;&#9472; GitHub Actions triggers again
       &#9500;&#9472;&#9472; Sanity checks on all 28 device configs
       &#9492;&#9472;&#9472; Batfish parses every .cfg for invalid syntax
6. CI passes &#8594; run compliance &#8594; build Config Plan
7. Deploy to one device per platform
8. Verify &#8594; expand in waves</code></code></pre><p>Steps 1 through 3 catch template bugs. Step 5 catches data bugs (a device in Nautobot with missing or malformed SoT data that produces an invalid config). Both run automatically. Both block the pipeline if something is wrong.</p><p>The intended config validation is particularly useful because it checks real data. The mock contexts are representative, but they can&#8217;t cover every edge case across 28 devices. A VRF that&#8217;s missing a route-target, an interface with an unexpected prefix length, a BGP endpoint with no peering defined. Those only show up when you render against the real SoT, and Batfish catches them before they become a Config Plan.</p><h2><span>Validating the actual intended configs (not just mock renders)</span></h2><p>Everything above validates templates before they produce output. But there&#8217;s a second gate that matters just as much: validating what Nautobot actually generates.</p><p>When Nautobot&#8217;s intended job runs, it renders your templates against live SoT data for all 28 devices and pushes the results to <code>golden-config/intended-configs/</code>. That SoT data might have quirks the mocks don&#8217;t cover. A device someone added without a BGP routing instance. A VRF missing its route-targets. An interface with a /28 mask that the template only handles /24, /31, and /32.</p><p>We add a second test file that scans the actual generated configs:</p><pre><code><code># tests/test_intended_configs.py

INTENDED_CONFIGS = collect_intended_configs()  # finds all .cfg files

class TestIntendedConfigSanity:
    """Basic checks, no Batfish needed."""

    def test_config_not_empty(self, platform, cfg_path):
        content = cfg_path.read_text()
        assert len(content.strip()) &gt; 200  # catch render failures

    def test_no_none_in_config(self, platform, cfg_path):
        # Python None leaking into config = missing | default() filter

    def test_no_jinja_artifacts(self, platform, cfg_path):
        # {{ or {% in output = partial render failure

    def test_starts_with_hostname(self, platform, cfg_path):
    def test_ends_with_end(self, platform, cfg_path):

class TestIntendedConfigsBatfish:
    """Feed all 28 configs to Batfish."""

    def test_no_failed_parses(self, batfish_results):
    def test_no_unexpected_parse_warnings(self, batfish_results):
    def test_no_undefined_references_ios(self, batfish_results):
</code></code></pre><p>Running it against our actual lab output:</p><pre><code><code>$ pytest tests/test_intended_configs.py -v
...
143 passed in 2.37s</code></code></pre><p>28 devices, 5 sanity checks each, plus 3 Batfish assertions covering all of them. Every single intended config parsed cleanly. The only warnings were the same <code>ip ssh bulk-mode</code> line on IOS-XE devices, which we already know is benign.</p><p>The GitHub Actions workflow triggers on pushes to <code>golden-config/intended-configs/**</code>. So the flow is: Nautobot runs the intended job, commits and pushes the rendered configs, GitHub Actions validates them, and you get a green or red check before you ever build a Config Plan.</p><h2><span>What this means for network engineering</span></h2><p>We just moved golden config development from &#8220;push and pray&#8221; to something that looks like a real software development lifecycle. The templates are code. They have tests. The tests run on every commit. Broken configs get rejected before they can reach a device.</p><p>This isn&#8217;t theoretical. In the SP demo lab, the BGP template alone is 140 lines of Jinja2 with 8 levels of nested logic handling route-reflector-client decisions, PE-CE VRF peering, VPNv4 and VPNv6 address families. A single missing variable or misplaced <code>{% endif %}</code> in that template would have required a full redeploy cycle to diagnose. Now it fails in under a second with a clear error message pointing at the exact line.</p><p>The tools are free. j2lint, pytest, and Batfish are all open source. The GitHub Actions minutes for this workload are negligible. The only ongoing cost is maintaining the mock contexts when templates evolve, and that cost is strictly less than the cost of a failed deployment.</p><h2><span>What comes next</span></h2><p>These checks answer one question: should this configuration be allowed into the deployment workflow? They don&#8217;t replace a reviewed Config Plan, a staged rollout, or protocol verification.</p><p>Part 3 publishes Thursday for paid subscribers. We configure Nautobot credentials and Nornir, deploy to the Cisco core in waves, then verify IS-IS, MPLS LDP, VPNv4, VPNv6, and PE-to-CE BGP.</p><div><hr></div><p><em>This free companion is part of the SP Demo Lab series. All code is in the <a href="https://github.com/byrn-baker/blog-sandbox">blog-sandbox</a> repo.</em></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.byrnbaker.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Network Plumber is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[What I'm Building Here (Start Here)]]></title><description><![CDATA[28 devices, one source of truth, zero manual config, and an AI agent that diagnoses outages before I wake up.]]></description><link>https://www.byrnbaker.me/p/what-im-building-here-start-here</link><guid isPermaLink="false">https://www.byrnbaker.me/p/what-im-building-here-start-here</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Fri, 14 Aug 2026 16:31:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!N1Rf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm taking a full service provider network from empty database to AI-monitored NOC, and writing down every step.</p><p>The series is 10 parts. By the end, we'll have 28 devices running ISIS, MPLS, BGP, and EVPN. Configs generated from a source of truth. Monitoring that catches failures across layers. And an AI agent that receives alerts, pulls route tables and interface state, and writes up what broke without anyone logging into a router.</p><p>All of it runs on one Proxmox box. No cloud. No physical gear beyond a single server.</p><p>Here's what the topology looks like: an MPLS L3VPN core connecting 3 datacenter customers. Each customer site runs a leaf-spine EVPN/VXLAN fabric. 13 Cisco IOS-XE routers and 15 Arista EOS switches. The monitoring stack is Prometheus, Loki, Grafana, and OTel Collector, with device inventory pulled from Nautobot. The AI piece is NetClaw, an agent I built that receives alerts from Alertmanager, queries all those systems for context, and produces root cause analysis.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!N1Rf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!N1Rf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 424w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 848w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 1272w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!N1Rf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg" width="1456" height="953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/37079099-b4ff-4bec-ab25-9d359910e370.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:953,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130887,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/svg+xml&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://networkplumber.substack.com/i/211201935?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!N1Rf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 424w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 848w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 1272w, https://substackcdn.com/image/fetch/$s_!N1Rf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F37079099-b4ff-4bec-ab25-9d359910e370.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>What I'm showing across the series:</p><p>1. SoT-driven infrastructure where every device, IP, cable, and BGP session lives in Nautobot and everything else queries it</p><p>2. Config generation from templates, not manual CLI</p><p>3. Failure detection across network, transport, and application layers</p><p>4. AI root cause analysis pulling state from before and after the alert</p><p>5. Config compliance and drift detection</p><p>6. The whole thing reproducible on one box</p><p>I believe the next generation of network engineers won't get the apprenticeship I had. Nobody's going to hand them an expensive network and say "learn by doing" for two years. So I'm building the lab, the walkthroughs, and eventually a rentable environment where you can follow along hands-on.</p><p><strong>Free subscribers</strong> get post previews and occasional full unlocks. <strong>Paid subscribers</strong> get every walkthrough, every config, every template. <strong>Founding Members</strong> get all of that plus 20% off lab rentals when they launch, and priority on what I cover next.</p><p>If you learn by building, you're in the right place.</p>]]></content:encoded></item><item><title><![CDATA[Building an AI-Monitored NOC from Scratch]]></title><description><![CDATA[Part 2: Setting up the Nautobot Golden Config App]]></description><link>https://www.byrnbaker.me/p/building-an-ai-monitored-noc-from</link><guid isPermaLink="false">https://www.byrnbaker.me/p/building-an-ai-monitored-noc-from</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Fri, 14 Aug 2026 01:26:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Lv4F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Lv4F!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Lv4F!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Lv4F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg" width="1168" height="784" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:784,&quot;width&quot;:1168,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:288047,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://networkplumber.substack.com/i/211122866?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Lv4F!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Lv4F!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9c571e81-2e15-495b-b441-5bc8831be056_1168x784.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>TLDR:</strong> Part 2 of 10. We wire Nautobot Golden Config to our blog-sandbox GitHub repo, write modular Jinja2 templates that pull ISIS/MPLS/BGP data via GraphQL plus config contexts for operational parameters, and generate intended configurations for all 28 devices - entirely from Source of Truth data. No spreadsheets, no manual config, no Ansible. One &#8220;Generate Intended Configs&#8221; button produces the complete SP core and DC fabric configurations.</p></blockquote>
      <p>
          <a href="https://www.byrnbaker.me/p/building-an-ai-monitored-noc-from">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[From Zero to AI-Monitored NOC: Starting with the Source of Truth]]></title><description><![CDATA[Setting up Nautobot]]></description><link>https://www.byrnbaker.me/p/from-zero-to-ai-monitored-noc-starting</link><guid isPermaLink="false">https://www.byrnbaker.me/p/from-zero-to-ai-monitored-noc-starting</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Wed, 12 Aug 2026 19:26:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/16b034dc-99f6-4066-b97c-e321f8041ac9_1168x784.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote><p><strong>TLDR:</strong> This is Part 1 of a 10-part series building a full service provider MPLS/EVPN lab (28 devices in CML) with AI-driven monitoring. In this post we deploy Nautobot 3.2.1 with Design Builder, BGP Models, IGP Models, and Golden Config, then populate the entire topology (devices, IPs, cabling, VRFs) in a single idempotent job run. Everything downstream, config generation, monitoring inventories, AI investigation, queries this one source of truth.</p></blockquote><h2><span>What we&#8217;re building and why</span></h2><p>This series builds a complete service provider network from scratch and then monitors it the way a modern NOC would. An AI agent investigates alerts, correlates state history, and produces root cause analysis without a human touching a CLI.</p><p><strong>The network</strong>: an MPLS L3VPN core with 3 datacenter customers, each running a leaf-spine EVPN/VXLAN fabric with K3s clusters and distributed applications. 28 devices total: 13 Cisco IOS-XE routers (CAT8000v) and 15 Arista EOS switches (vEOS), all running in CML on Proxmox.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.byrnbaker.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3Pyp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3Pyp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 424w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 848w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 1272w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3Pyp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg" width="1456" height="953" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:953,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:130887,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/svg+xml&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://networkplumber.substack.com/i/210942422?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3Pyp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 424w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 848w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 1272w, https://substackcdn.com/image/fetch/$s_!3Pyp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd3bc0c6b-676d-4739-9e59-06640dcf0a1b.svg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p><em>The MPLS L3VPN core: P routers, PE routers, and route reflectors connecting three customer sites.</em></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nfwu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nfwu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 424w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 848w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 1272w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nfwu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg" width="1456" height="790" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da971306-79da-44ba-bf8a-33d492180690.svg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:790,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89056,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/svg+xml&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://networkplumber.substack.com/i/210942422?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nfwu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 424w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 848w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 1272w, https://substackcdn.com/image/fetch/$s_!nfwu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda971306-79da-44ba-bf8a-33d492180690.svg 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;63d8c03f-f93a-4f1f-81f8-6608a3106314&quot;,&quot;duration&quot;:null}"></div><p></p><p><em>Each datacenter customer runs a leaf-spine EVPN/VXLAN fabric with K3s workloads.</em></p><p><strong>The monitoring</strong>: a full observability stack (Prometheus, Loki, Grafana, OTel Collector) pulling inventory from a real Source of Truth (Nautobot), plus SuzieQ for historical state queries. When an alert fires, the system can ask &#8220;what did the BGP table look like 5 minutes before this happened?&#8221;</p><p><strong>The AI NOC</strong>: NetClaw, an AI agent that receives alerts from Alertmanager, queries the monitoring stack and SuzieQ for context, and posts a root cause analysis to a diary, all without operator intervention for defined alert types.</p><p><strong>What I hope to demonstrate by the end:</strong></p><ol><li><p><strong>SoT-driven infrastructure</strong>: every device, IP, cable, and BGP session lives in Nautobot. Config templates, monitoring inventories, and investigations all query the same truth.</p></li><li><p><strong>Failure detection across layers</strong>: a single link failure cascades through network (BGP/IGP reconvergence), transport (latency spike), and application (replication lag) layers, and the stack sees all three.</p></li><li><p><strong>AI-assisted root cause analysis</strong>: when a PE-CE link drops, the agent doesn&#8217;t just report &#8220;BGP peer down.&#8221; It queries what the route table, LLDP neighbors, and interface state looked like <em>before</em> the alert, identifies the specific interface that failed, and reports whether traffic rerouted or the customer is isolated.</p></li><li><p><strong>Configuration compliance</strong>: Nautobot Golden Config holds the intended state, detects drift, and the stack can remediate with assert-before-and-after validation via SuzieQ.</p></li><li><p><strong>Reproducible by anyone</strong>: everything runs on a single Proxmox host with CML. No cloud accounts, no physical switches, no vendor licenses beyond the CML images.</p></li></ol><p>The series is 10 parts. This is Part 1: we stand up Nautobot as the source of truth and populate it with the complete topology using a Design Builder job, so every subsequent part can query it programmatically instead of maintaining spreadsheets.</p><div><hr></div><h2><span>Why start with the Source of Truth</span></h2><p>Every step that follows, configuring routers, deploying monitoring, wiring up AI investigation needs to know what devices exist, where they are, what IPs they have, and how they&#8217;re connected. Nautobot is the system that holds that data and makes it queryable by every tool in the stack.</p><div><hr></div><h2><span>Deploy Nautobot 3.2.1</span></h2><h3><span>1. Clone nautobot-docker-compose</span></h3><pre><code><code>cd ~
git clone https://github.com/nautobot/nautobot-docker-compose.git
cd nautobot-docker-compose
</code></code></pre><h3><span>2. Pin Nautobot to 3.2.1</span></h3><p>Edit <code>pyproject.toml</code> and set the Nautobot version:</p><pre><code><code>[tool.poetry.dependencies]
nautobot = "3.2.1"
</code></code></pre><h3><span>3. Add the apps</span></h3><pre><code><code>poetry add nautobot-design-builder
poetry add nautobot-bgp-models
poetry add nautobot-golden-config
poetry add nautobot-igp-models
</code></code></pre><p>This updates <code>pyproject.toml</code> and <code>poetry.lock</code> with all four apps and their dependencies resolved against Nautobot 3.2.1.</p><h3><span>4. Enable the apps in nautobot_config.py</span></h3><p>Edit <code>config/nautobot_config.py</code>:</p><pre><code><code>PLUGINS = [
    "nautobot_design_builder",
    "nautobot_bgp_models",
    "nautobot_golden_config",
    "nautobot_igp_models",
]

PLUGINS_CONFIG = {
    "nautobot_plugin_nornir": {
        "connection_options": {
            "napalm": {
                "extras": {
                    "optional_args": {
                        "global_delay_factor": 1,
                        "transport": "ssh"
                    },
                },
            },
            "netmiko": {
                "extras": {
                    "global_delay_factor": 1,
                    "fast_cli": False,
                    "read_timeout_override": 30,
                    "disabled_algorithms": {"pubkeys": ["rsa-sha2-256", "rsa-sha2-512"]},
                },
            },
        },
        "nornir_settings": {
            "credentials": "nautobot_plugin_nornir.plugins.credentials.nautobot_secrets.CredentialsNautobotSecrets",
            "runner": {
                "plugin": "threaded",
                "options": {
                    "num_workers": 20,
                },
            },
        },
    },
    "nautobot_golden_config": {
        "per_feature_bar_width": 0.15,
        "per_feature_width": 13,
        "per_feature_height": 4,
        "enable_backup": True,
        "enable_compliance": True,
        "enable_intended": True,
        "enable_sotagg": True,
        "enable_plan": True,
        "enable_deploy": True,
        "enable_postprocessing": True,
        "sot_agg_transposer": None,
        "postprocessing_callables": ['nautobot_golden_config.utilities.config_postprocessing.render_secrets'],
        "postprocessing_subscribed": [],
        "jinja_env": {
            "undefined": "jinja2.StrictUndefined",
            "trim_blocks": True,
            "lstrip_blocks": False,
            "extensions": ["jinja2.ext.do"],
        },
        "default_framework": {"all": "netmiko"},
        "get_config_framework": {"all": "netmiko"},
        "default_deploy_status": "Not Approved",
    }
}
</code></code></pre><h3><span>5. Build and start</span></h3><pre><code><code>invoke build --no-cache
invoke start
</code></code></pre><p>Wait ~2 minutes for migrations (the BGP/IGP/Golden Config models add database tables on first start), then verify:</p><pre><code><code>curl -s http://localhost:8080/health/
</code></code></pre><p>Log in at <code>http://&lt;your-vm-ip&gt;:8080</code>. You should see:</p><ul><li><p><strong>Routing &#8594; BGP</strong> in the nav (BGP Models)</p></li><li><p><strong>Routing &#8594; IGP</strong> in the nav (IGP Models)</p></li><li><p><strong>Golden Config</strong> in the nav</p></li><li><p><strong>Design Builder</strong> under Extensibility or Jobs</p></li></ul><div><hr></div><h2><span>Why these four apps</span></h2><p>AppWhat it gives youUsed in<strong>Design Builder</strong>Declarative bulk data population from YAML templatesPart 1: populate the full topology in one job run<strong>BGP Models</strong>ASN, BGP Routing Instances, Peer Groups, Peerings, Address FamiliesPart 2&#8211;3: model all iBGP/eBGP sessions<strong>IGP Models</strong>OSPF/IS-IS instances, areas, interface configsPart 2: model the IS-IS underlay<strong>Golden Config</strong>Intended config generation, backup, compliance checkingPart 10: drift detection and remediation</p><p>All four have Nautobot 3.0+ compatibility releases.</p><div><hr></div><h2><span>Load the SP Demo Lab design</span></h2><p>The Design Builder job lives in the <code>jobs/</code> folder of <code>nautobot-docker-compose</code>. Nautobot picks up any Python job modules in that folder on startup.</p><h3><span>Copy the design into the jobs folder</span></h3><pre><code><code># From wherever you have the design files
cp -r sp_demo_lab ~/nautobot-docker-compose/jobs/
</code></code></pre><p>The structure inside <code>jobs/</code>:</p><pre><code><code>jobs/
  sp_demo_lab/
    __init__.py          # DesignJob class + register_jobs()
    context/
      __init__.py        # All 28 devices, links, VRFs from addressing plan
    designs/
      0001_foundations.yaml.j2  # Locations, roles, platforms, device types, VRFs
      0002_devices.yaml.j2      # 28 devices with interfaces + IPs
      0003_cabling.yaml.j2      # Cables + P2P IP assignments
</code></code></pre><h3><span>Restart to pick up the new job</span></h3><pre><code><code>cd ~/nautobot-docker-compose
invoke stop start
</code></code></pre><h3><span>Run the design</span></h3><ol><li><p><strong>Jobs &#8594; SP Demo Lab - Full Topology &#8594; Run</strong></p></li><li><p>The job renders the templates with the context data and creates all objects</p></li><li><p>Re-running is idempotent. Existing objects are updated, not duplicated</p></li></ol><div><hr></div><h2><span>What the design creates</span></h2><p>Object typeCountDetailsLocations5SP-Demo-Lab (region), SP-Core, DC-A, DC-B, DC-C (sites)Device Roles7P-Router, PE-Router, Route-Reflector, CE-Router, Border-Router, Spine, LeafPlatforms2cisco_iosxe, arista_eosDevice Types2CAT8000v, vEOSDevices2813 Cisco CAT8000v + 15 Arista vEOSInterfaces~250Management, Loopback0, data interfaces per deviceIP Addresses~100Management, loopbacks, P2P /31 assignmentsPrefixes~20Management, SP core, DC fabricsVRFs4MGMT-VRF, CUST-A, CUST-B, CUST-CCables~40All inter-device links</p><div><hr></div><h2><span>Verify</span></h2><h3><span>UI quick checks</span></h3><ul><li><p><strong>Devices</strong> &#8594; 28 total, filter by site to verify distribution</p></li><li><p><strong>IPAM &#8594; Prefixes</strong> &#8594; <code>10.0.0.0/24</code> has child /31s</p></li><li><p><strong>IPAM &#8594; IP Addresses</strong> &#8594; loopbacks and P2P links populated</p></li><li><p><strong>Cables</strong> &#8594; all inter-device connections present</p></li><li><p><strong>Routing &#8594; BGP</strong> &#8594; empty for now (Part 2 populates this)</p></li></ul><div><hr></div><h2><span>Lifecycle management</span></h2><ul><li><p><strong>Update</strong>: edit <code>context/__init__.py</code> with new devices or IPs, restart Nautobot, re-run the job &#8594; objects updated in place</p></li><li><p><strong>Decommission</strong>: Design Builder &#8594; Deployments &#8594; select &#8594; Decommission &#8594; all objects from that run removed cleanly</p></li><li><p><strong>Add a device</strong>: add it to the context list, re-run. No need to touch 4 different places.</p></li></ul><div><hr></div><h2><span>What you have now</span></h2><p>LayerStateNautobot 3.2.1Running with BGP Models, IGP Models, Golden Config, Design BuilderSoT data28 devices, full IPAM, cabling, VRFs populatedNetwork devicesManagement IPs up, no production config yetMonitoringNot deployed yet (Part 6)NetClawNot deployed yet (Part 8)</p><p><strong>Next: Part 2, SP Core: IS-IS, MPLS, and BGP</strong></p><div><hr></div><h2><span>Reference</span></h2><ul><li><p>nautobot-docker-compose: https://github.com/nautobot/nautobot-docker-compose</p></li><li><p>Design Builder docs: https://docs.nautobot.com/projects/design-builder/</p></li><li><p>BGP Models docs: https://docs.nautobot.com/projects/bgp-models/</p></li><li><p>Golden Config docs: https://docs.nautobot.com/projects/golden-config/</p></li><li><p>IGP Models: https://github.com/byrn-baker/nautobot-app-igp-models</p></li><li><p>Blog Sandbox: https://github.com/byrn-baker/blog-sandbox/</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.byrnbaker.me/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>