<?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>Fri, 09 Oct 2026 13:30:29 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[From Counters to Conversations: NetFlow, sFlow, OpenTelemetry, and SuzieQ]]></title><description><![CDATA[How NetFlow, sFlow, and OpenTelemetry help connect network traffic to application behavior.]]></description><link>https://www.byrnbaker.me/p/from-counters-to-conversations-netflow</link><guid isPermaLink="false">https://www.byrnbaker.me/p/from-counters-to-conversations-netflow</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 01 Oct 2026 23:07:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!O7al!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png" 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_!O7al!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O7al!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 424w, https://substackcdn.com/image/fetch/$s_!O7al!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 848w, https://substackcdn.com/image/fetch/$s_!O7al!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 1272w, https://substackcdn.com/image/fetch/$s_!O7al!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O7al!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png" width="1456" height="762" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:762,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1945676,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/218417584?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!O7al!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 424w, https://substackcdn.com/image/fetch/$s_!O7al!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 848w, https://substackcdn.com/image/fetch/$s_!O7al!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 1272w, https://substackcdn.com/image/fetch/$s_!O7al!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F455da262-ce19-4e0a-bbaa-2ad02ca2e652_1734x907.png 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>In Part 7, I got the routers and switches into Grafana, the dashboard I use to view the lab&#8217;s monitoring data. I could check interface traffic, errors and discards, and see the state of the routing sessions. That gave me a place to start when something went wrong.</p><p>The next thing I wanted was context for those graphs. If traffic between the datacenters increased, which hosts were responsible? What were they talking to?</p><p>Looking at network state raised another set of questions. BGP is the routing protocol these devices use to exchange reachability information. Seeing an established session tells me two devices are communicating, but I might still need to know whether a destination is in the routing table and where the device will send its packets next. I wanted to search that information across the Cisco routers and Arista switches without opening a console on each one.</p><p>Those questions led me to NetFlow, sFlow and SuzieQ. NetFlow summarizes the traffic a router observes. sFlow exports samples of traffic passing through a device. SuzieQ collects operational information from the devices and makes it available through a common query interface.</p><p>I already had an OpenTelemetry Collector receiving SNMP measurements. The Collector is a service that receives telemetry, processes it and sends it to storage. Adding flow collection would give me traffic details alongside the interface measurements. SuzieQ would give me a place to inspect routes, neighbors and interfaces across both platforms.</p><p>I&#8217;ll follow a file transfer between two datacenters, find its traffic in the collected data, and use Nautobot to put names beside the IP addresses. You don&#8217;t need prior experience with these tools to follow the example. Running it yourself assumes the earlier lab setup: devices, Nautobot inventory and the Kubernetes monitoring stack. The configurations below are from that lab, not a fresh-install guide.</p>
      <p>
          <a href="https://www.byrnbaker.me/p/from-counters-to-conversations-netflow">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[From Network Devices to Grafana: Building the Lab’s SNMP Pipeline]]></title><description><![CDATA[Introducing ArgoCD and GitOps]]></description><link>https://www.byrnbaker.me/p/from-network-devices-to-grafana-building</link><guid isPermaLink="false">https://www.byrnbaker.me/p/from-network-devices-to-grafana-building</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 01 Oct 2026 22:16:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fGQc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png" 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_!fGQc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fGQc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fGQc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1830990,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/216392238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fGQc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!fGQc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fed68ebec-bff3-4b69-8ffc-e535d3b1449c_1672x941.png 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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;fd13fde2-e8bf-43db-9faa-6ff3dee8f811&quot;,&quot;duration&quot;:null}"></div><p></p><p>After following packets through QEMU and the Arista leaves, I wanted to watch the network without opening a console on every device. I wanted to select CE1 in Grafana, find <code>GigabitEthernet2</code>, and see whether its traffic, errors or discards changed while Longhorn moved data between sites.</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><p>Getting a collector pod to start would not answer that question. Its requests had to reach the devices&#8217; management routing tables, the devices had to permit them, and storage had to accept the measurements. Then the dashboard had to show an interface I could recognize from the router&#8217;s command-line interface (CLI).</p><p>I finished with fresh measurements from all 28 routers and switches and a network dashboard whose interface names match the CLI. I also added the cluster and storage measurements I needed beside those graphs, then organized Grafana around the systems I would investigate. I extended that collection to BGP peers, IS-IS neighbors and Cisco BFD sessions, checking the results against the CLI before trusting the dashboard. This post follows the path into Grafana, including the gaps I found in the devices&#8217; SNMP tables and the event history I still need for brief routing failures.</p><p><a href="https://vscode-remote+ssh-002dremote-002b192-002e168-002e3-002e21.vscode-resource.vscode-cdn.net/home/ubuntu/demo-blog/part6-ansible-k3s-bind-from-nautobot.md">Part 6</a> covers the original K3s installation and Nautobot-backed Ansible inventory. The free companion, <a href="https://vscode-remote+ssh-002dremote-002b192-002e168-002e3-002e21.vscode-resource.vscode-cdn.net/home/ubuntu/demo-blog/virtual-lab-troubleshooting-qemu-gro.md">The Interfaces Were Up. The Packets Had Stopped.</a>, explains the QEMU and Arista packet failures I repaired first. Here I build on those foundations to collect and display the network measurements.</p><p>K3s is the Kubernetes distribution running the applications. Longhorn stores their persistent volumes, VictoriaMetrics stores measurements over time, and Grafana displays them. Argo CD keeps the Kubernetes resources aligned with their declarations in Git. SNMP supplies the device measurements; OpenTelemetry (OTel) carries them to storage.</p><h2><strong>A place to run the monitoring system</strong></h2><p>The cluster has three masters in DC-A and six workers across DC-B and DC-C. The masters run the Kubernetes API and embedded etcd, the database holding cluster state. They remain together because the packet fixes did not establish consistently low latency under cross-site storage load. Loss of DC-A still means loss of the control plane.</p><p>The hosts use K3s <code>v1.30.5+k3s1</code> and <code>bond0</code> for Flannel traffic. I disabled the bundled Traefik and ServiceLB so the ingress components described later have their own Git-managed configuration. Flannel connects the pod networks over VXLAN. Its MTU is 8950 over the 9000-byte host data interfaces.</p><p>I needed each correction to survive the next deployment. Host configuration, device commands and Kubernetes resources have different owners, so I kept their sources separate:</p><p>Repository sourceWhat it controls<a href="https://github.com/byrn-baker/blog-sandbox/tree/main/ansible/">blog-sandbox/ansible/</a>Ubuntu networking, K3s and standalone BIND<a href="https://github.com/byrn-baker/blog-sandbox/tree/main/golden-config/templates/">blog-sandbox/golden-config/templates/</a>Device configuration generated from Nautobot<a href="https://github.com/byrn-baker/blog-sandbox/tree/main/telemetry/">blog-sandbox/telemetry/</a>Generated SNMP collector values and dashboard<a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/blob/main/bootstrap/argocd-values.yaml">blog-sandbox-argo-cd/bootstrap/argocd-values.yaml</a>Argo CD&#8217;s own Helm installation<a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/apps/">blog-sandbox-argo-cd/apps/</a>Child Applications discovered by the root<a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/values/">blog-sandbox-argo-cd/values/</a>Workload Helm configuration<a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/observability/">blog-sandbox-argo-cd/observability/</a>Cluster and storage collection, health alerts and dashboard folders</p><p>The <a href="https://github.com/byrn-baker/blog-sandbox/blob/main/demo-lab/06a-ansible-automation.md">Ansible workflow</a> obtains host addresses and roles from Nautobot. Neither a second static inventory nor manual edits to generated device configurations are needed.</p><h2><strong>Giving the recovered cluster a declared application state</strong></h2><p>Part 6 had already modeled host and K3s configuration in Git. For Kubernetes applications, I needed a controller to keep reconciling those declarations after the initial install. With GitOps, I put the desired resources in a repository and let the controller work toward that state. Reverting a commit can restore earlier resource declarations; it does not restore application data from a volume.</p><p>Argo CD represents each managed application with an <code>Application</code> custom resource: a repository, a path or chart, a revision, and a destination. Point one Application at a directory of other Application manifests and it becomes an app-of-apps. I apply the root once, then add workloads through files in Git.</p><h2><strong>Standing up Argo CD</strong></h2><p>Helm packages Kubernetes resource definitions into a chart, with a values file supplying configuration. I use the chart mirror prepared in Part 6 and keep Argo CD&#8217;s settings in <code>bootstrap/argocd-values.yaml</code>. The examples below assume both repositories are checked out on the automation host and <code>kubectl</code> is using the lab&#8217;s Kubernetes context.</p><p>I placed Argo CD&#8217;s components in DC-A, on the same three nodes that already hold etcd and the API server. The node selector below limits placement to that site. The toleration permits these pods to run on nodes reserved for the control plane:</p><pre><code><code>global:
  nodeSelector:
    kubernetes.io/os: linux
    topology.kubernetes.io/zone: dc-a
  tolerations:
    - key: node-role.kubernetes.io/control-plane
      operator: Equal
      value: "true"
      effect: NoSchedule
</code></code></pre><p>DC-A was reserved for the control plane in Part 6. Argo CD is a deliberate exception. The application controller talks continuously to the API, and keeping its repo-server and Redis in the same site avoids spreading that internal traffic across DC-A, DC-B and DC-C. I checked the available capacity before placing it there. The storage and application workloads run on the workers in DC-B and DC-C. The host-level monitoring collectors described later run on the nodes they monitor, including the masters.</p><p>The bootstrap values also select <code>docker.io/library/redis</code>, which uses the Docker Hub cache configured in Part 6. With the values in place, install Argo CD from the pinned chart:</p><pre><code><code>cd /home/ubuntu/blog-sandbox-argo-cd
helm upgrade --install argocd \
  http://192.168.3.21:8888/helm/argo-cd-10.8.0.tgz \
  --namespace argocd --create-namespace \
  --values bootstrap/argocd-values.yaml \
  --wait --timeout 10m
kubectl -n argocd get pods
</code></code></pre><p>The chart version and mirror address belong to this lab. If you&#8217;re adapting the repository, check those values and the DC-A node labels against your cluster first. Before continuing, confirm the Argo CD pods are Ready.</p><h2><strong>Making the first sync wait for storage</strong></h2><p>Argo CD keeps comparing Git declarations with live Kubernetes resources. One root Application, applied by hand once, watches a directory in <code>blog-sandbox-argo-cd</code> and auto-discovers everything else:</p><pre><code><code>spec:
  source:
    repoURL: https://github.com/byrn-baker/blog-sandbox-argo-cd.git
    targetRevision: main
    path: apps
    directory:
      recurse: false
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
</code></code></pre><p>The first two children were Longhorn at sync-wave 1 and VictoriaMetrics/Grafana at wave 2. The monitoring deployment adds <code>cluster-observability</code> at wave 3, <code>snmp-metrics</code> at wave 4 and <code>otel-snmp</code> at wave 5. MetalLB and the ingress resources follow at waves 9 through 12. A sync wave is a numbered group applied before a higher-numbered group. Storage must be ready before the applications request volumes, and the stores should be ready before the collector starts exporting.</p><p><a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/health/#argocd-app">Argo CD needs Application health handling</a> for the parent to use child health in this pattern. I installed the customization from <code>bootstrap/argocd-values.yaml</code> and checked it in the live <code>argocd-cm</code>.</p><p>The chart-based child Applications point <code>repoURL</code> at the mirror directory containing <code>index.yaml</code>. The <code>chart</code> and <code>targetRevision</code> fields select the package. For Longhorn, those fields are:</p><pre><code><code>repoURL: http://192.168.3.21:8888/helm/
chart: longhorn
targetRevision: 1.12.1
</code></code></pre><p>A direct <code>.tgz</code> URL works in the Helm install command above, but it isn&#8217;t a Helm repository URL for an Argo Application.</p><p>Longhorn also needs this setting in <code>values/longhorn-values.yaml</code>:</p><pre><code><code>preUpgradeChecker:
  jobEnabled: false
</code></code></pre><p>This disables an upgrade-check hook that otherwise runs before its service account exists during the initial Argo installation. The <a href="https://github.com/longhorn/longhorn/issues/6415">Longhorn issue</a> explains the hook conflict.</p><p>Once Argo is Ready, apply the root and watch its children:</p><pre><code><code>kubectl apply -f bootstrap/root-app.yaml
kubectl -n argocd get applications -w
</code></code></pre><p><code>Synced</code> means the deployed resources match Git; <code>Healthy</code> means they meet Argo&#8217;s health checks. Confirm Longhorn and VictoriaMetrics have both statuses before moving on to collection. The collector also needs the credential Secret created in the generation step below. The Application health customization controls the initial ordering through the root. It doesn&#8217;t serialize every later update: existing children can auto-sync independently.</p><p>Argo&#8217;s own bootstrap values sit outside the root&#8217;s <code>apps/</code> path. To change those settings later, rerun the Helm command with the updated values.</p><h2><strong>A running application still needed working storage</strong></h2><p>Longhorn keeps volume replicas, copies on separate workers, and attaches the volume to the application pod. A PersistentVolumeClaim (PVC) is the application&#8217;s request for storage. Kubernetes attachment state and Longhorn replica health answer different questions, so I checked both:</p><pre><code><code># From the automation host with its existing lab Kubernetes context
kubectl get nodes -o wide
kubectl get pvc -A
kubectl get volumeattachments
kubectl -n longhorn-system get volumes.longhorn.io
kubectl -n longhorn-system get pods
</code></code></pre><p>Before starting collection, check that the claims are Bound, the volumes are attached where needed, and Longhorn reports healthy replicas. Also check free space in both the Ubuntu guests and the hypervisor&#8217;s storage pool. Space inside a VM doesn&#8217;t tell you whether the underlying pool can accept more writes.</p><p>Grafana&#8217;s values use a Recreate deployment strategy, which stops the old pod before starting its replacement, and a longer startup allowance for database initialization. Those settings live in <code>values/victoria-metrics-values.yaml</code>. They let Kubernetes wait for Grafana to initialize its persistent database before treating a slow start as a failure.</p><h2><strong>Getting the devices to answer</strong></h2><p>SNMP, the Simple Network Management Protocol, lets a collector request measurements such as interface byte counters, errors and uptime. My network scope was 13 Cisco IOS-XE routers and 15 Arista EOS switches. Nautobot also held ten Ubuntu servers, but those were outside this device-polling rollout.</p><p>I already used Nautobot to generate device configuration, so I used the same model for SNMP access. A config context supplies structured values that templates turn into commands. The community, contact and allowed source network were shared across all seven network-device roles; location came from each device&#8217;s own record. One context was enough. The community below is a placeholder:</p><pre><code><code>snmp:
  ro_community: "&lt;snmp-read-only-community&gt;"
  contact: "byrn-baker demo-lab"
  acl_source: "192.168.3.0/24"
  acl_source_wildcard: "0.0.0.255"
</code></code></pre><p>I used SNMPv2c for this lab&#8217;s first collection path, with read-only access and a source access control list (ACL), which permits requests from specified addresses. A community is the shared string the collector presents to an SNMPv2c agent. It travels without encrypted authentication. The ACL limits where requests may originate; it does not give v2c the security properties of SNMPv3. The choice kept this first deployment&#8217;s scope small, and it should remain an explicit lab tradeoff.</p><p>I permit the management VLAN because the collector can move between workers. On the captured polling path, requests left the worker&#8217;s <code>eth0</code> interface using its management IP. When adapting the ACL, check the source address of actual SNMP requests; a pod&#8217;s internal address may be translated before the device sees it.</p><p>The ACL syntax differs between platforms:</p><p>{% raw %}</p><pre><code><code>{# ios/snmp.j2 #}
ip access-list standard ACL-SNMP-RO
 permit {{ config_context.snmp.acl_source.split('/')[0] }} {{ config_context.snmp.acl_source_wildcard }}
</code></code></pre><p>{% endraw %}</p><p>{% raw %}</p><pre><code><code>{# eos/snmp.j2 #}
ip access-list standard ACL-SNMP-RO
   10 permit {{ config_context.snmp.acl_source }}
</code></code></pre><p>{% endraw %}</p><p>The two templates look similar, but the devices expect different address syntax. EOS accepts CIDR, which writes a network and prefix length together, such as <code>192.168.3.0/24</code>. IOS expects the network address followed by a wildcard mask, which marks the address bits the rule can ignore.</p><p>I validate the rendered commands before deploying them:</p><pre><code><code># From the blog-sandbox checkout
make ci
make ci-full
</code></code></pre><p>The first command checks template structure. The second adds Batfish configuration checks, which can catch invalid device syntax even when Jinja renders successfully. The active compliance definitions live in <code>jobs/gc_compliance_setup/__init__.py</code>.</p><h3><strong>EOS needed the management VRF and the ACL</strong></h3><p>A Virtual Routing and Forwarding instance (VRF) has its own routing table. The EOS management interfaces belong to <code>MGMT-VRF</code>, so SNMP also has to be enabled in that routing context to answer requests there. The <a href="https://github.com/byrn-baker/blog-sandbox/commit/7163fbf">template correction</a> emits one <code>snmp-server vrf &lt;name&gt;</code> line per unique VRF on a modeled Management interface. The resulting device configuration includes:</p><pre><code><code>ip access-list standard ACL-SNMP-RO
   10 permit 192.168.3.0/24
snmp-server community &lt;snmp-read-only-community&gt; ro ACL-SNMP-RO
snmp-server vrf MGMT-VRF
</code></code></pre><p>I deploy both the <code>snmp</code> and <code>acl</code> features. The SNMP feature covers <code>snmp-server</code> commands, but the access list is a separate configuration block. Deploying just the community&#8217;s ACL reference doesn&#8217;t install the restriction itself.</p><p>After syncing the source into Nautobot, I regenerate intended configuration, collect fresh backups, run compliance and generate Config Plans. I check one device per platform before expanding to the fleet, with Fail Job on Task Failure enabled. Live reads and successful polls confirmed the VRF binding and ACL on all 15 EOS devices.</p><h3><strong>Check that the settings will survive a restart</strong></h3><p>Running configuration is active now; startup configuration is what the device loads after reboot. I compare both, because polling can work even when the saved configuration is incomplete:</p><pre><code><code>show running-config
show startup-config
</code></code></pre><p>Check the SNMP community, ACL and EOS management VRF in both outputs. If a save is needed, review the rest of the running configuration first so unrelated changes aren&#8217;t saved accidentally:</p><pre><code><code>copy running-config startup-config
</code></code></pre><p>Then reconnect and read startup configuration again. Nautobot&#8217;s intended files, backups and per-device job logs help with that comparison, but a successful job status doesn&#8217;t replace the device readback.</p><h2><strong>Generate the collector from the same device model</strong></h2><p>The generator selects IOS-XE and EOS devices from Nautobot, using their roles, management addresses, sites and SNMP contexts. The ten Ubuntu servers are excluded from this 28-device scope. I use one collector with two export destinations, so writing to two stores does not double the polling load on devices. The completed configuration has 28 device receivers and three additional BGP-only receivers for the SERVERS VRF, covered later in the post.</p><p>The following commands are the repository&#8217;s generation workflow. Start in the parent directory of adjacent <code>blog-sandbox</code> and <code>blog-sandbox-argo-cd</code> checkouts. They require its Python dependencies, the existing Nautobot credentials in the environment, a Kubernetes context pointing at the lab, and the observability namespace and VictoriaMetrics operator already installed. <code>--sync-secret</code> writes the externally managed Kubernetes credential Secret; this is a deployment step, not a read-only preview.</p><pre><code><code>cd blog-sandbox
python3 telemetry/generate_snmp.py \
  --canary CE1 DCA-Leaf01 \
  --output ../blog-sandbox-argo-cd/values/otel-snmp-values.yaml \
  --sync-secret
pytest telemetry/test_generate_snmp.py -q
</code></code></pre><p>The canary selects one Cisco router and one Arista leaf from the same model. After checking their measurements, the full-fleet generation uses the same command without <code>--canary CE1 DCA-Leaf01</code>. I review the generated non-secret diff, render it with the pinned chart and validate the collector configuration with the pinned binary before publishing the values to the Argo repository. The root Application discovers the declared children; I do not apply a separate collector Deployment by hand.</p><p>The <a href="https://github.com/byrn-baker/blog-sandbox/blob/main/telemetry/README.md">generator runbook</a> records dependencies, pinned artifacts and credential rotation. Credentials go into <code>network-snmp-credentials</code> in namespace <code>observability</code>, while Git holds the references. Synchronizing that Secret does not change the SNMP community on a router or switch.</p><p>The collector uses memory-backed export queues. A restart can lose queued samples and creates a polling gap; this deployment does not promise lossless delivery during outages. Freshness, polling errors and queue metrics make those gaps visible.</p><h2><strong>Following a measurement into storage</strong></h2><p>Once the canaries answered, I could follow their measurements through the rest of the path. The OpenTelemetry Collector polls every 60 seconds, with staggered starts so all devices do not answer at once. It forwards samples using OTLP, the OpenTelemetry Protocol, over HTTP to two VictoriaMetrics stores. The existing store holds cluster metrics; the second keeps a separate, longer history for SNMP. Grafana queries the stored measurements.</p><pre><code><code>13 IOS-XE routers + 15 EOS switches
                 |
        SNMP over management VLAN
                 |
           OTel Collector
                 |
             OTLP/HTTP
            /         \
  cluster metrics     SNMP history
            \         /
               Grafana
</code></code></pre><p>The fleet rollout produced samples from all 28 devices in both stores. Each device had at least five uptime samples in the five-minute check, and the oldest latest sample was less than 60 seconds old. The collector&#8217;s export queues were empty, its refused-point counters were zero, and it reported no failed exports. That established more than a running pod: device replies had become stored measurements I could query.</p><p>The dedicated SNMP store is configured for 547 days of retention on a 20 GiB volume. I kept it separate because extending retention for every Kubernetes metric would have a different storage cost. The cluster store retains its existing one-month setting. A retention setting does not prove that the disk can hold that much history; I still need to measure growth. A time series is one metric with a particular set of labels, such as incoming bytes on one interface of one device. More series and more samples consume more space.</p><h2><strong>The interface name had to mean the same thing on the router</strong></h2><p>The first dashboard returned measurements, but its legends exposed the entire label set: device, address, interface index, abbreviated name and collector metadata. That was information the storage system needed, presented as though it were an interface name. I still had to work out which line belonged to the port I wanted to inspect.</p><p>I compared the collected names with <code>show interfaces</code> on all 28 devices. SNMP&#8217;s interface Management Information Base, or IF-MIB, gives each interface row an index and several descriptive fields. A MIB defines the objects a device exposes through SNMP. The index identifies a row for polling and joining counters; it is not a useful label for a graph.</p><p>The two name fields also differed. On CE1, <code>ifName</code> contained <code>Gi2</code>, while <code>ifDescr</code> contained <code>GigabitEthernet2</code>. On DCA-Leaf01, both contained <code>Ethernet1</code>. My collector already stored <code>ifDescr</code> in the <code>interface</code> label, so I could correct the presentation without changing the stored measurements.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vJqB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 424w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 848w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 1272w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vJqB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png" width="987" height="147" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:147,&quot;width&quot;:987,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:27944,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/216392238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vJqB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 424w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 848w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 1272w, https://substackcdn.com/image/fetch/$s_!vJqB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5aaae205-3f15-4a8b-9fe6-4ca75956aa1d_987x147.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>I made the Device and Interface selectors use those full names. Traffic legends now pair the device with its interface, and the status table has three useful columns: Device, Interface and Interface status. Numeric states render as Up, Down or the corresponding named condition. Error and discard panels show rates in errors/s and discards/s. The internal labels remain available for diagnosis without taking over the dashboard.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_9kI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_9kI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_9kI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Network SNMP filtered to CE1 GigabitEthernet2, with full interface names in the traffic legends&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Network SNMP filtered to CE1 GigabitEthernet2, with full interface names in the traffic legends" title="Network SNMP filtered to CE1 GigabitEthernet2, with full interface names in the traffic legends" srcset="https://substackcdn.com/image/fetch/$s_!_9kI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!_9kI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1f42827-70e6-4151-9989-9096e38b2838_1600x1200.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>Here I selected CE1 and <code>GigabitEthernet2</code>. The traffic panels follow that selection; the freshness counter still checks all 28 devices. The <a href="https://raw.githubusercontent.com/byrn-baker/blog-sandbox-argo-cd/main/observability/evidence/part7/part7-snmp-arista.png">Arista view</a> uses the same layout with <code>DCA-Leaf01 / Ethernet1</code>.</p><h3><strong>Why 372 SNMP rows became 318 displayed interfaces</strong></h3><p>The name comparison exposed another distinction: an SNMP interface row does not always represent another port. The stored data contained 372 rows. Of those, 28 described the Multiprotocol Label Switching (MPLS) layer on Cisco interfaces, and 26 were IOS <code>Null0</code> or <code>VoIP-Null0</code> sinks, internal destinations for discarded traffic. The remaining 318 names appeared in the devices&#8217; <code>show interfaces</code> output.</p><p>MPLS forwards traffic using labels attached to packets. Its SNMP rows describe that protocol layer, so they could represent traffic already counted on the underlying interface. Plotting them as additional links would make the network appear busier than it was. They also lacked error and discard counters. On the inspected SP1 row, a direct request returned <code>noSuchInstance</code>: the requested object identifier, or OID, did not have a value there. The physical interface returned a real counter. Missing and zero meant different things.</p><p>I excluded the MPLS rows and IOS null sinks from the normal interface selectors and panels. I kept the raw measurements and a separate table identifying MPLS rows without error counters. That left a view I could compare with the CLI without throwing away diagnostic data.</p><p>There was one gap in the other direction. Each Arista leaf showed an internal <code>Vlan4097</code> in its CLI output, but those nine interfaces were absent from the collected IF-MIB rows. I documented the exception rather than adding empty graphs that would imply collection was working for them.</p><h3><strong>Keeping dashboard changes in Git</strong></h3><p>I made these changes in <code>telemetry/network_dashboard.py</code>, the dashboard&#8217;s source. The generated JSON travels inside a ConfigMap in the Collector&#8217;s Helm values, then reaches Grafana through Argo CD. Editing the dashboard only in Grafana would leave the next deployment able to replace my changes.</p><p>The generator escapes Grafana&#8217;s legend placeholders so Helm preserves them when rendering the ConfigMap. I check the rendered chart as well as the dashboard JSON, because the dashboard passes through both formats before Grafana reads it.</p><p>For this presentation change, I used the generator&#8217;s dashboard-only mode from the <code>blog-sandbox</code> checkout:</p><pre><code><code>python3 telemetry/generate_snmp.py --dashboard-only \
  --output ../blog-sandbox-argo-cd/values/otel-snmp-values.yaml
</code></code></pre><p>This updates the dashboard while preserving the receiver configuration and credential revision. Review and publish the values through the Argo repository, then check the result in Grafana with one Cisco interface and one Arista interface selected. Both should show full names, readable status values and explicit rate units.</p><p>My browser and query checks covered both platforms. The <a href="https://github.com/byrn-baker/blog-sandbox/tree/main/telemetry/evidence/snmp-readability">audit and screenshots</a> record the CLI comparison and rendered result.</p><h2><strong>Giving each part of the lab a place in Grafana</strong></h2><p>Once I could recognize the network interfaces, I needed to make the rest of Grafana useful too. The chart had placed its dashboards together in General. Looking for a storage problem meant searching past cluster views, dashboards for the metrics stores and panels for operating systems I wasn&#8217;t running.</p><p>I organized them around where I would go to investigate a problem:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yF02!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yF02!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 424w, https://substackcdn.com/image/fetch/$s_!yF02!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 848w, https://substackcdn.com/image/fetch/$s_!yF02!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 1272w, https://substackcdn.com/image/fetch/$s_!yF02!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yF02!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png" width="1395" height="402" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:402,&quot;width&quot;:1395,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:103959,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/216392238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yF02!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 424w, https://substackcdn.com/image/fetch/$s_!yF02!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 848w, https://substackcdn.com/image/fetch/$s_!yF02!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 1272w, https://substackcdn.com/image/fetch/$s_!yF02!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff063ecf-a301-439d-895b-1f56b482c54a_1395x402.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!k0-_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!k0-_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 424w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 848w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 1272w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!k0-_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png" width="1456" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Grafana dashboard browser with K3s, Network, Observability and Storage folders&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Grafana dashboard browser with K3s, Network, Observability and Storage folders" title="Grafana dashboard browser with K3s, Network, Observability and Storage folders" srcset="https://substackcdn.com/image/fetch/$s_!k0-_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 424w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 848w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 1272w, https://substackcdn.com/image/fetch/$s_!k0-_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30b45ed9-13bb-4900-b0ef-a4e6d563ba33_1600x550.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>That left 39 dashboards and none in General. I removed the Windows, AIX, macOS, multi-cluster and Prometheus-server dashboards because those systems weren&#8217;t deployed here. The existing Linux and Kubernetes detail views stayed available when I needed to move beyond the overview.</p><h3><strong>A folder needed data behind it</strong></h3><p>The control-plane dashboards exposed a collection gap. K3s runs etcd, the scheduler and controller manager inside its server process. The scheduler chooses where pods run; the controller manager keeps resources aligned with their requested state. The chart&#8217;s normal discovery expected separate component pods, so those scrapes had no targets. A scrape is the collector&#8217;s request to a component&#8217;s metrics endpoint. An empty target list meant I had no measurement, not that the component had stopped.</p><p>I added a small vmagent collector on each master to read the existing loopback listeners: port 2381 for etcd, 10257 for the controller manager and 10259 for the scheduler. Loopback is the host&#8217;s local network connection. These collectors share the host&#8217;s network namespace so they can reach it without exposing the listeners elsewhere or restarting K3s. They send samples to the existing cluster metrics store every 30 seconds. The configuration is in <code>observability/control-plane.yaml</code>.</p><p>Longhorn&#8217;s endpoint was available too, but its NetworkPolicy, the Kubernetes rule controlling incoming connections, blocked the monitoring agent. I added a rule admitting that agent to the managers&#8217; shared API and metrics port, then collected all six managers. I also added metrics from five Argo CD components, including its application controller and repository server. The <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/observability/">collection manifests</a> keep the endpoint selection and access rules together.</p><p>Now I can start with a container restart in the K3s folder and compare its time window with API latency and etcd disk writes. The Storage folder shows whether a volume was rebuilding or experiencing slow I/O. Network gives me the interface counters for the same window. That comparison helps narrow the next check; a host-wide retransmission counter still cannot identify which connection lost data.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!073P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!073P!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!073P!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!073P!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!073P!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!073P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;K3s Stability dashboard showing node readiness, component scrape coverage, restarts, API latency and etcd disk latency&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="K3s Stability dashboard showing node readiness, component scrape coverage, restarts, API latency and etcd disk latency" title="K3s Stability dashboard showing node readiness, component scrape coverage, restarts, API latency and etcd disk latency" srcset="https://substackcdn.com/image/fetch/$s_!073P!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!073P!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!073P!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!073P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4b194a47-6b79-4e5a-b824-70a1bf73aea0_1600x1200.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>This view shows a one-hour window. All nine nodes remain Ready and each control-plane component has three scrape targets, but the restart and API-latency panels still contain activity worth investigating. Readiness tells me the nodes are available; it doesn&#8217;t mean every request was fast or every container ran without interruption. The p99 latency estimates the value below which 99% of the measured requests fall.</p><h3><strong>Keeping the folders through the next deployment</strong></h3><p>I kept dashboard JSON under <code>blog-sandbox-argo-cd/observability/dashboards/</code>. The <code>observability/kustomization.yaml</code> file packages each dashboard into a ConfigMap with a <code>grafana_folder</code> annotation identifying the destination folder. The SNMP generator adds the same annotation for Network. Grafana&#8217;s sidecar, a helper container, copies those files into the corresponding directories.</p><p>Grafana&#8217;s file provider checks for dashboard updates every 30 seconds. The sidecar loads the files during startup and continues watching for changes. These settings live in <code>values/victoria-metrics-values.yaml</code>; a Git-managed migration hook removes the old chart-owned dashboards to avoid duplicate IDs.</p><p>After deployment, I checked the folders in Grafana and ran all 26 queries on the three new overview dashboards. Each returned data, and browser checks found no query errors or empty panels. The <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/observability/evidence/">verification record and screenshots</a> show the result.</p><h2><strong>Reaching Grafana by name</strong></h2><p>I use </p><p>https://grafana.sandbox.lab</p><p> for Grafana and </p><p>https://argocd.sandbox.lab</p><p> for Argo CD. Both names share an ingress address. MetalLB assigns that address to a Kubernetes LoadBalancer Service and announces it on the network; Traefik receives the web request and uses its hostname to select the application.</p><pre><code><code>Browser -&gt; DNS lookup -&gt; ingress IP -&gt; Traefik -&gt; Grafana or Argo CD
</code></code></pre><p>The lab has two ingress addresses, one on each network:</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CWMk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 424w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 848w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 1272w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CWMk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png" width="752" height="147" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:147,&quot;width&quot;:752,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25351,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/216392238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CWMk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 424w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 848w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 1272w, https://substackcdn.com/image/fetch/$s_!CWMk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F98a2a28b-486c-427d-86e8-44f50d2d585f_752x147.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>The root Application deploys MetalLB, its address pools, Traefik and the application routes from the <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/ingress">ingress manifests</a>. The bundled K3s Traefik and ServiceLB remain disabled because I manage this Traefik installation through Argo CD and use MetalLB for address assignment. Two Traefik replicas run on workers in different sites. MetalLB announces the management address on <code>eth0</code> and the lab address on <code>bond0</code>.</p><p>BIND supplies the DNS answers. Its views return the lab address for queries sourced from the lab&#8217;s <code>10.0.0.0/8</code> network and the management address for management clients. A view is a set of DNS answers selected by the query&#8217;s source address. If a resolver forwards the query, BIND sees that resolver&#8217;s address.</p><p>The names and addresses are declared in <code>blog-sandbox/service_endpoints.yml</code>. To publish them, run the BIND playbook with the existing Nautobot credential environment:</p><pre><code><code>cd /home/ubuntu/blog-sandbox/ansible
.ansible/bin/ansible-playbook pb.dns.yml -e bind_validate_only=true
.ansible/bin/ansible-playbook pb.dns.yml
</code></code></pre><p>The first command validates staged DNS configuration without activating it. The second publishes it. My workstation uses pfSense for DNS, so I added a domain override under <strong>Services &gt; DNS Resolver &gt; Domain Overrides</strong>: <code>sandbox.lab</code> points to the BIND server at <code>192.168.3.71</code>, with TLS Queries disabled. That destination is the DNS server, not the ingress address. The resolver must also be able to send queries through the management interface.</p><p>HTTPS uses a private lab certificate authority (CA), which signs the ingress certificate. After Argo creates the Traefik namespace, the certificate helper creates or reuses the local keys and installs the required Kubernetes Secrets:</p><pre><code><code>cd /home/ubuntu/blog-sandbox-argo-cd
python3 tools/ingress_tls.py
</code></code></pre><p>Import the public <code>ingress/lab-ingress-ca.crt</code> into the workstation&#8217;s trusted certificate authorities. Private keys stay out of Git. Certificate renewal is a separate maintenance step covered in the <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/blob/main/ingress/README.md">ingress runbook</a>.</p><p>From the workstation, check the DNS answer and then open Grafana:</p><pre><code><code>nslookup grafana.sandbox.lab
</code></code></pre><p>The expected answer is <code>192.168.3.240</code>. In Grafana, open the Network folder and select Network SNMP. Both Grafana and Argo CD still require their application logins. My ingress checks reached both applications through the lab address from all three datacenters and verified their HTTPS certificates.</p><h2><strong>Adding routing state to the same collection path</strong></h2><p>An interface can remain Up while the routing relationship using it fails. That mattered after the troubleshooting companion exposed brief BFD-triggered BGP resets under load. BFD, Bidirectional Forwarding Detection, checks whether a peer remains reachable. BGP, Border Gateway Protocol, exchanges routes and can use BFD to react quickly when a neighbor stops responding. Interface counters alone couldn&#8217;t tell me whether those sessions stayed established.</p><p>I started by asking the devices what they could return. BGP peer-state queries worked on Cisco and Arista. My first IS-IS query came back empty, but that wasn&#8217;t enough to conclude the router couldn&#8217;t expose it. IS-IS builds neighbor relationships called adjacencies, and Cisco placed the usable adjacency table under its own MIB branch. A Management Information Base, or MIB, defines the objects available through SNMP; each object has a numeric identifier in that tree. The standard branch and Cisco&#8217;s branch were different places to look.</p><p>The working Cisco state column was <code>1.3.6.1.4.1.9.10.118.1.6.1.1.2</code>. I added it to <code>telemetry/routing_metrics.py</code>, alongside BGP state, time established and a counter of entries into Established. Nautobot&#8217;s device roles determine which receivers get each profile. The P routers don&#8217;t run BGP, so polling an empty BGP table on those four devices would add no useful measurement.</p><p>I checked CE1, DCA-Leaf01 and SP1 before expanding. Their existing SNMP access already allowed these reads through the management network. The change was in the collector configuration, deployed through Argo CD, and the samples followed the same path into both VictoriaMetrics stores.</p><h3><strong>Checking what the numbers actually represented</strong></h3><p>SP1 exposed five IS-IS adjacencies. To name their interfaces, I followed the circuit index to its IF-MIB index, then used the same full interface description as the interface dashboard. That produced <code>GigabitEthernet2</code> rather than an unexplained circuit number. The mapping comes from each poll, so I didn&#8217;t need to maintain a separate list of circuit-to-port assignments.</p><p>BFD caught a problem with that approach. SP1 returned ten SNMP rows, while <code>show bfd neighbors</code> showed five sessions. Each session appeared under two application IDs, 9 and 15. The reported BFD interface indices also disagreed with IF-MIB. Joining those values would have put the wrong interface names beside otherwise plausible Up states.</p><p>I kept the BFD local discriminator instead. This is the identifier shown in the CLI&#8217;s <code>LD/RD</code> column: LD is the local discriminator and RD is the remote one. The dashboard retains the application rows and counts distinct local discriminators separately. Across the core, 56 application rows represented 28 CLI sessions. I didn&#8217;t turn those duplicate rows into extra links or invent protocol names for the application IDs.</p><p>The completed polling check looked like this in both metrics stores:</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1QqX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 424w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 848w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 1272w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1QqX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png" width="1375" height="320" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:320,&quot;width&quot;:1375,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64150,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/216392238?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1QqX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 424w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 848w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 1272w, https://substackcdn.com/image/fetch/$s_!1QqX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbd2dc1ac-00b1-4ff6-8885-e5b3a4ef7cfb_1375x320.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>The default SNMP table omitted the SERVERS-VRF peer on each Leaf03 switch. On these EOS devices, polling with the existing community followed by <code>@SERVERS</code> returned the missing peer. I added a BGP-only receiver for each verified context, using the same credential Secret, and checked its state, remote AS, uptime and transition counter against direct SNMP reads. The peer address and Established state also matched <code>show ip bgp summary vrf all</code>.</p><p>The collection targets live in <code>telemetry/bgp_vrf_profiles.yaml</code>. The generator verifies that each configured VRF exists on that device&#8217;s modeled interfaces in Nautobot. Each measurement carries a <code>vrf</code> label so identical peer addresses in different routing tables remain separate. These metrics cover the verified peer transports; they don&#8217;t report how many prefixes each address family exchanges.</p><h3><strong>Reading the result in Grafana</strong></h3><p>I added <strong>Network Routing</strong> beside <strong>Network SNMP</strong> in the Network folder. BGP rows show the device, VRF, peer address, exact remote AS number and readable state. The BGP VRF selector switches between All, default and SERVERS without changing the IS-IS or BFD panels. Uptime and transition graphs include the VRF in their legends. IS-IS rows show the full local interface name. BFD rows use the local discriminator described above. The <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/blob/main/observability/dashboards/Network/network-routing.json">dashboard JSON</a> keeps those choices in Git.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vvh4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vvh4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vvh4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png" width="1456" height="1092" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1092,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Network Routing dashboard filtered to the three SERVERS-VRF peers, with full IS-IS interface names and Cisco BFD application rows&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Network Routing dashboard filtered to the three SERVERS-VRF peers, with full IS-IS interface names and Cisco BFD application rows" title="Network Routing dashboard filtered to the three SERVERS-VRF peers, with full IS-IS interface names and Cisco BFD application rows" srcset="https://substackcdn.com/image/fetch/$s_!vvh4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 424w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 848w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!vvh4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa41f2f4b-c9a9-4384-84fd-260c6139dca8_1600x1200.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>The screenshot selects SERVERS, so it shows three BGP peers. Selecting All shows 136. IS-IS and BFD retain their own counts because the VRF selector applies only to BGP. Remote AS values remain exact numbers, such as <code>65001</code>, rather than abbreviated measurements.</p><p>For the transition graph, I use VictoriaMetrics&#8217; <code>increase_prometheus</code> function to calculate changes between observed counter samples. Starting a new VRF time series must not turn its existing lifetime counter into an apparent burst of recent flaps. The newly labeled graphs begin with this collection rollout; they don&#8217;t reconstruct earlier VRF history.</p><p>The <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/blob/main/observability/routing-rules.yaml">routing rules</a> also check reported non-up states and row counts below the verified baseline for each device. That second check matters when a failed neighbor disappears from a table instead of remaining there with a Down value. Default-context coverage stays separate from the three SERVERS peers, which also have alerts for a missing device/VRF/peer combination. The baseline must change when I deliberately change the topology. I verified rule evaluation, not delivery to an external notification destination.</p><h2><strong>What a successful poll still could not tell me</strong></h2><p>A session can go down and recover between one-minute polls. BGP&#8217;s established-transition counter gives me another clue when that happens, but it doesn&#8217;t explain why BFD expired. I still need event history beside these graphs.</p><p>A poll starts with a request from the collector. An SNMP trap is a notification sent by the device when something happens. Syslog carries event messages, often including the peer, routing context and reason. These need receivers of their own; adding a trap destination to a switch wouldn&#8217;t make the OTel SNMP polling receiver accept it.</p><p>I checked the notification catalog on the Arista canaries with:</p><pre><code><code>show snmp notification
</code></code></pre><p>EOS 4.34.6M listed BGP transitions, interface link-up/link-down and IS-IS adjacency notifications. It didn&#8217;t list a BFD notification. Its logs did contain <code>%BFD-5-STATE_CHANGE</code> messages with the peer, VRF and diagnostic reason, so syslog was the source I could follow for those BFD events.</p><p>The trap receiving side was still missing. None of the four inspected canaries had an <code>snmp-server host</code> destination, and the cluster had no trap listener or UDP 162 Service. I hadn&#8217;t demonstrated end-to-end trap delivery or deliberately interrupted a routing session during these checks.</p><p>That next collection path needs a destination reachable through the management VRF, a listener that decodes traps, and syslog ingestion for the BFD details already present on the switches. VictoriaLogs and its planned 14-day retention belong there. I&#8217;d start with a safe test notification from one device per platform and check that the stored event retains its device, peer, VRF, interface and reason before expanding.</p><h2><strong>Knowing whether collection is still working</strong></h2><p>A Ready collector means its process passed a health check. I also need to know whether a device answered recently. In Grafana&#8217;s Explore view, using the SNMP datasource, this query counts devices with an uptime sample less than three minutes old:</p><pre><code><code>count(time() - timestamp(snmp_device_uptime_ticks{job="snmp"}) &lt; 180)
</code></code></pre><p>For this fleet, I expect 28. A smaller count tells me to find the stale devices before interpreting their traffic graphs. For interface coverage, I use the same exclusions as the normal dashboard view:</p><pre><code><code>count by (device) (
  snmp_interface_oper_status{
    job="snmp", if_type!="", if_type!="166",
    interface!~"(VoIP-)?Null0"
  }
)
</code></code></pre><p>The <code>if_type</code> value 166 identifies the MPLS protocol-layer rows. To inspect a particular port, I select its full name. This query converts the incoming byte counter on CE1&#8217;s GigabitEthernet2 into bits per second over a five-minute window:</p><pre><code><code>rate(snmp_interface_in_octets_total{
  job="snmp", device="CE1", interface="GigabitEthernet2",
  if_type!="", if_type!="166"
}[5m]) * 8
</code></code></pre><p>The dashboard also shows sample age, polling errors and export queues. A missing sample must remain unknown, not turn into zero traffic. Reported interface speed needs the same care: the speed advertised by a virtual NIC is not a measurement of the virtual topology&#8217;s forwarding capacity.</p><p>The <a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/observability/evidence/part7/">current verification and screenshots</a> show all ten Argo Applications Synced and Healthy, all nine K3s nodes Ready, and all three Longhorn volumes attached and healthy with three read/write replicas each. Each control-plane component has three successful scrape targets, Longhorn has six and the selected Argo CD components have five. SNMP supplies fresh measurements from all 28 network devices, including 133 default-context and three SERVERS-VRF BGP peers.</p><p>I checked the dashboard queries and the pages they render. The three cluster, storage and Argo overview dashboards returned data for all 26 queries; the routing dashboard passed all 45 checks across its All, default and SERVERS selections. Coverage alerts check missing collection, unhealthy volumes and restarts of Longhorn&#8217;s storage-driver containers. Rule evaluation works, but delivery to an external notification destination remains untested.</p><p>I kept the first release small enough to follow all the way through: one device per platform, a stored measurement, a recognizable interface on a graph, then the rest of the fleet. The startup readback and missing-counter checks showed where a green status could conceal unfinished work. The browser check exposed a different problem: data could be present and still be awkward to use.</p><p>I can now select the same interface in Grafana that I would inspect at the CLI and watch its counters while the applications use the network. BGP and IS-IS state now sit beside those counters. Capturing routing events is the next collection step. Flow collection and SuzieQ remain outside this release. The <a href="https://vscode-remote+ssh-002dremote-002b192-002e168-002e3-002e21.vscode-resource.vscode-cdn.net/home/ubuntu/demo-blog/virtual-lab-troubleshooting-qemu-gro.md">free troubleshooting companion</a> explains the failures that made those measurements necessary; this build gives me the first part of the record for the next investigation.</p><h2><strong>References</strong></h2><ul><li><p><a href="https://www.byrnbaker.me/p/the-interfaces-were-up-the-packets?r=2juhig">Part 6: One Cluster, Three Datacenters, Zero Static Inventory</a></p></li><li><p><a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/health/#argocd-app">Argo CD Application health</a></p></li><li><p><a href="https://github.com/longhorn/longhorn/issues/6415">Longhorn and Argo CD pre-upgrade hook issue</a></p></li><li><p><a href="https://github.com/byrn-baker/blog-sandbox/blob/main/telemetry/README.md">SNMP generator, storage and validation runbook</a></p></li><li><p><a href="https://github.com/byrn-baker/blog-sandbox/tree/main/telemetry/evidence/snmp-readability">Interface naming, event-source audit and dashboard screenshots</a></p></li><li><p><a href="https://github.com/byrn-baker/blog-sandbox-argo-cd/tree/main/observability/">Cluster monitoring, dashboard folders and live verification</a></p></li><li><p><a href="https://docs.victoriametrics.com/metricsql/#increase_prometheus">VictoriaMetrics counter calculations</a></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">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[Netclaw on Omarchy]]></title><description><![CDATA[Connecting an AI Agent to My Network Lab]]></description><link>https://www.byrnbaker.me/p/netclaw-on-omarchy</link><guid isPermaLink="false">https://www.byrnbaker.me/p/netclaw-on-omarchy</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Sun, 27 Sep 2026 16:38:40 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/217695437/31413cbdc8a28d4f641c8f325aeecc6e.mp3" length="0" type="audio/mpeg"/><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_!HOwy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HOwy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HOwy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1944344,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/217695437?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HOwy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!HOwy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd633e72d-66cf-45dc-a939-b73423a48ee9_1672x941.png 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>I set up Netclaw on Omarchy and connected it to my existing Cisco and Arista network lab. In this video, I walk through the setup, a health-check report across 28 devices, and a test of access to the telemetry I already collect through Grafana.</p><p>The interesting part is what those results actually mean. Loading an inventory, collecting device output, and confirming that an integration works are separate checkpoints. I show what worked and where the setup still needed attention.</p><p>The lab uses Nautobot for inventory, pyATS for device checks, and an observability stack that includes SNMP, NetFlow, sFlow, OpenTelemetry, and SuzieQ.</p><p>Music: &#8220;Molecule&#8221; by AGST, via Epidemic Sound.</p>]]></content:encoded></item><item><title><![CDATA[Who Actually Runs Your Container?]]></title><description><![CDATA[Kubernetes from First Principles: How a Workload Becomes a Pod]]></description><link>https://www.byrnbaker.me/p/who-actually-runs-your-container</link><guid isPermaLink="false">https://www.byrnbaker.me/p/who-actually-runs-your-container</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Sun, 20 Sep 2026 20:02:03 GMT</pubDate><enclosure url="https://api.substack.com/feed/podcast/216628249/bc8ed4f3ce1beb561130067134b59b39.mp3" length="0" type="audio/mpeg"/><content:encoded><![CDATA[<p>I follow one real CoreDNS workload through a nine-node K3s cluster: from a Deployment submitted to the Kubernetes API, through controller reconciliation and scheduler placement, to the kubelet and containerd on the selected node.</p><p>Along the way, I separate a Deployment, ReplicaSet, Pod, and container; show how a Service gives replaceable Pods a stable address; and explain why <code>Running</code>, <code>Ready</code>, and healthy are different claims. I also show where embedded etcd fits in this K3s control plane and what K3s changes about packaging without changing the Kubernetes API model.</p><p>The live checks in this episode are read-only and point-in-time. Replacement and failure behavior use labeled illustrations. A <code>Ready</code> result is not presented as proof that every network, DNS, storage, or workload path is healthy.</p><p>Companion article: <a href="https://www.byrnbaker.me/p/one-cluster-three-datacenters-zero">One Cluster, Three Datacenters, Zero Static Inventory.</a></p>]]></content:encoded></item><item><title><![CDATA[The Interfaces Were Up. The Packets Had Stopped.]]></title><description><![CDATA[Sometimes you have to stop and really dig into a problem to understand the platform.]]></description><link>https://www.byrnbaker.me/p/the-interfaces-were-up-the-packets</link><guid isPermaLink="false">https://www.byrnbaker.me/p/the-interfaces-were-up-the-packets</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Tue, 15 Sep 2026 13:02:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JDvH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png" 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_!JDvH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JDvH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JDvH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1908142,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JDvH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!JDvH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F369d53ae-3178-4757-8aea-03ac9f4934c1_1536x1024.png 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>I was working on getting SNMP data into Grafana, using the nine-node K3s cluster in my lab. K3s runs the containers, and Longhorn keeps copies of their storage volumes on different workers. Before I could finish the monitoring setup, I had to sort out why those workers kept losing traffic between datacenters.</p><p>I had already changed the MTUs and switched the Cisco routers to virtio interfaces. I thought I had checked the network after those changes. Then SPE3, one of the routers in my service-provider core, stopped receiving packets on both core interfaces. Both still showed up, and their transmit counters kept increasing.</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><p>Rebooting SPE3 brought IS-IS back up and the large pings started working again. IS-IS is the routing protocol my core routers use to discover neighbors and calculate paths. But the receive counters stopped a second time. Meanwhile, the K3s nodes could report Ready while Longhorn rebuilds stalled and TCP kept retransmitting.</p><p>So I started following the packets through the virtual topology. That took me into QEMU, the process emulating the Cisco router hardware, and then into the Linux networking inside the Arista switches. I found a receive stall in the first and malformed combined TCP segments in the second. Getting past the QEMU problem was what let me see the leaf problem clearly.</p><p>I&#8217;ll walk through the captures and checks below. There is still a storage-load issue to investigate, so I&#8217;ll also show where the successful tests stopped answering the question. The companion <a href="https://vscode-remote+ssh-002dremote-002b192-002e168-002e3-002e21.vscode-resource.vscode-cdn.net/home/ubuntu/demo-blog/part7-argocd-observability-snmp.md">Part 7 implementation post</a> covers the SNMP collection system I built on the recovered lab.</p><h2><strong>Following the retransmissions down to a virtual NIC</strong></h2><p>The C8000V routers were using virtio, a network interface designed for virtual guests, with data-port maximum transmission units (MTUs) of 9216 bytes. I kept that configuration while checking the receive failure. I wanted to change one thing at a time and see what it did to the same failing traffic.</p><p>The MTU sets the packet-size limit at an interface. My lab uses jumbo frames, larger than the usual 1500-byte Ethernet setting. TCP breaks a transfer into segments and sends bytes again if they are not acknowledged. Those retransmissions told me the transfer was having trouble, but I still needed to find where the packets were being lost or damaged.</p><h3><strong>Map the layers the connection crosses</strong></h3><p>Take a connection from a K3s node in DC-A to one in DC-B. Both Linux VMs run on Proxmox. Their data interfaces connect to Arista vEOS leaves in EVE-NG, while the Cisco C8000V core runs in Cisco Modeling Labs (CML). A leaf is the switch the server connects to.</p><p>There are VMs inside VMs here. Proxmox hosts CML, and CML hosts the emulated routers. That means &#8220;the host&#8221; depends on which part I am checking: the CML VM is a guest from Proxmox&#8217;s point of view, but it is the host running the router&#8217;s QEMU process.</p><p>In the diagram, follow the connection from the source VM into its leaf, across the datacenter switching fabric, and through the CML core toward the other site. The management connection is separate. I could still use it to log into the devices when this data path stopped working..</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SQCv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SQCv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 424w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 848w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 1272w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SQCv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png" width="1456" height="697" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:697,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:202190,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SQCv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 424w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 848w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 1272w, https://substackcdn.com/image/fetch/$s_!SQCv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86a6b855-d6db-4bf7-86b8-997729432b35_2100x1005.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 1. I followed the data connection across each virtualization boundary. This simplified view omits redundant links and individual core hops; the management network is a separate diagnostic path.</em></p><p>The routing state helps identify which part of that path is working. In the core, I checked IS-IS adjacencies, the neighbor relationships between routers. BGP distributes routes, and its EVPN extension carries information about endpoints in the datacenter overlay. VXLAN is the tunnel carrying the original Ethernet frame across the underlying IP network. I needed to account for that wrapper when checking packet sizes and captures.</p><h3><strong>Account for the headers between layers</strong></h3><p>The host&#8217;s data interface is set to 9000 bytes, but Flannel, which connects Kubernetes pod networks, is set to 8950. That leaves room for Flannel&#8217;s tunnel headers. The router data ports are set to 9216. Follow the packet through the diagram and the different sizes make sense: each layer needs room for whatever the previous layer added.</p><p>TCP has another size to consider: maximum segment size, or MSS. This controls the amount of data in each TCP segment. I used it to send small segments and jumbo segments over the same path and compare the results.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qepr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qepr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 424w, https://substackcdn.com/image/fetch/$s_!qepr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 848w, https://substackcdn.com/image/fetch/$s_!qepr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 1272w, https://substackcdn.com/image/fetch/$s_!qepr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qepr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png" width="1456" height="614" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:614,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:159563,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!qepr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 424w, https://substackcdn.com/image/fetch/$s_!qepr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 848w, https://substackcdn.com/image/fetch/$s_!qepr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 1272w, https://substackcdn.com/image/fetch/$s_!qepr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a080209-84cf-4398-8fa1-c0bf1d98f76b_2100x885.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 2. I compared the limit at each layer. The different values leave room for encapsulation; they are not conflicting measurements of one packet.</em></p><p>I planned to capture the same traffic at the K3s guests, Proxmox, EVE and the CML core. For each path, I attempted four transfers, alternating requested MSS values of 1200 and 8960. Each sent 256 KiB in each direction at 1 Mbit/s. KiB and MiB describe amounts of data in binary units; Mbit/s describes millions of bits per second. I limited the rate because I wanted to check delivery before testing capacity.</p><p>Neither the DC-C to DC-A connection nor an alternate DC-B to DC-A connection established TCP. I could not compare the transfer rates yet. I went back to the core router that had stopped receiving.</p><h3><strong>Locate the stop inside CML</strong></h3><p>On SPE3, both core interfaces still showed <code>up/up</code> and MTU 9216. Look at the counters, though: transmitted packets kept increasing while received packets did not. Management traffic and the CE3-facing interface were still receiving. The failure affected the two core interfaces.</p><p>Both IS-IS neighbors were gone, and the BGP sessions to the route reflectors were Idle. Route reflectors redistribute routing information among BGP peers. Losing those sessions along with the receive counters pointed me toward the core interfaces, even though they still showed <code>up/up</code>.</p><p>I captured the SP1-SPE3 and SP3-SPE3 links in CML. Both ends were sending IS-IS hellos, the messages used to discover and maintain neighbors. The captures included Ethernet frames of 9229 bytes. That length includes link-layer overhead, so it is not the same number as the IP MTU.</p><p>The hellos were there on the links. I then needed to check whether QEMU was delivering them into SPE3.</p><p>I then checked how CML was passing those packets into SPE3. These interfaces use QEMU UDP sockets over loopback, so packets move between processes on the CML host. They do not need to leave CML and cross the outer Proxmox-to-EVE connection for this hop.</p><p>The sockets feeding the two failed interfaces had growing receive queues, roughly 12 MB and 15 MB of kernel queue accounting in the samples. The sockets for management and the CE-facing interface had no backlog. Packets were reaching CML and waiting there while the router&#8217;s receive counters stayed still.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!81b_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!81b_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 424w, https://substackcdn.com/image/fetch/$s_!81b_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 848w, https://substackcdn.com/image/fetch/$s_!81b_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!81b_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!81b_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png" width="1456" height="713" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:713,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:243214,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!81b_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 424w, https://substackcdn.com/image/fetch/$s_!81b_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 848w, https://substackcdn.com/image/fetch/$s_!81b_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!81b_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F84e04551-6405-4494-8c81-08f45c522c91_2100x1028.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 3. I narrowed the receive stall to the boundary between queued packets on the CML host and delivery into SPE3. The QEMU backport repaired this reproduced failure; it did not explain every later retransmission.</em></p><p>Here are the read-only commands for comparing the router&#8217;s counters with the CML host&#8217;s sockets and running executable. Run the IOS commands inside SPE3 and the Linux commands on the CML host. In the latter, <code>&lt;router-pid&gt;</code> is the process ID of the affected router&#8217;s QEMU process.</p><pre><code><code>! Inside the SPE3 IOS console
show interfaces GigabitEthernet2
show interfaces GigabitEthernet3
show isis neighbors
</code></code></pre><pre><code><code># In the Linux shell of the CML host
sudo ss -u -a -n -p -m
sudo readlink /proc/&lt;router-pid&gt;/exe
sudo sha256sum /proc/&lt;router-pid&gt;/exe
</code></code></pre><p>In the <code>ss</code> output, I looked for the sockets owned by SPE3&#8217;s QEMU process and matched them to the affected NICs. A total for every socket on the host would not tell me which interface was stuck.</p><p>The <code>/proc/&lt;router-pid&gt;/exe</code> checks answer a different question: which executable is this router actually running? Replacing <code>/usr/bin/qemu-system-x86_64</code> does not replace the code already loaded by a running process. SHA-256 gives me a fingerprint of the executable so I can compare the running copy with the one I tested.</p><p>The next place to look was QEMU&#8217;s receive ring. This is the set of buffers, regions of guest memory, that the router makes available for incoming packets. QEMU uses counters to track its position in the ring. These counters are 16 bits wide: after 65535, they return to zero.</p><p>The failed interfaces were all close to that rollover: three interfaces across two routers. Working interfaces kept advancing through their counters, so I compared the stuck ones more closely.</p><p>I read those counters through QMP, the QEMU Machine Protocol, using <code>virsh qemu-monitor-command</code>. <code>virsh</code> talks to libvirt, the VM-management layer. The QMP operation <code>x-query-virtio-queue-status</code> showed the consumed and cached available indices for queue 0 of each mapped NIC.</p><p>A name such as <code>/machine/peripheral/net2/virtio-backend</code> is a device path inside QEMU. It is not a Linux file, and <code>net2</code> needs to be matched to the router&#8217;s actual interface. I saved the queue-status readings before inspecting individual entries with <code>x-query-virtio-queue-element</code>, because that deeper check also refreshes QEMU&#8217;s cached available index. Here are the original readings:</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!avgr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 424w, https://substackcdn.com/image/fetch/$s_!avgr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 848w, https://substackcdn.com/image/fetch/$s_!avgr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 1272w, https://substackcdn.com/image/fetch/$s_!avgr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!avgr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png" width="1201" height="235" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:235,&quot;width&quot;:1201,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24639,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!avgr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 424w, https://substackcdn.com/image/fetch/$s_!avgr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 848w, https://substackcdn.com/image/fetch/$s_!avgr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 1272w, https://substackcdn.com/image/fetch/$s_!avgr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1fc97e5e-ff66-4049-b71d-0894b673c8a1_1201x235.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>Both SPE3 interfaces had a full set of 256 buffers waiting in the guest. The head buffers were 2060 bytes each, and the guest and QEMU had negotiated mergeable receive buffers, allowing several buffers to hold one large packet. There should have been space for the jumbo frame.</p><p>QEMU also reported that the NICs were enabled and unbroken. I sampled them again: the consumed indices stayed frozen while working interfaces advanced. Inspecting the individual queue entries refreshed the cached index, but did not get reception moving.</p><h3><strong>Why a counter rollover looked like an empty queue</strong></h3><p>The guest had supplied receive buffers, so why was QEMU behaving as though there was nowhere to put the packet? I followed that question into <code>virtqueue_num_heads()</code><a href="https://github.com/qemu/qemu/blob/v8.2.2/hw/virtio/virtio.c#L998-L1192"> and </a><code>virtqueue_split_get_avail_bytes()</code><a href="https://github.com/qemu/qemu/blob/v8.2.2/hw/virtio/virtio.c#L998-L1192"> in QEMU 8.2.2</a>. These functions help work out how many buffer entries are available and whether they provide enough space for the incoming packet.</p><p>The guest advertises its available buffers through a counter. QEMU keeps a cached copy so it does not have to reread guest memory for every check. &#8220;Cached&#8221; means a previously read value; it can fall behind when the guest supplies more buffers.</p><p>The scan has its own counter as it adds up the available space. This was the comparison I initially suspected: the guest&#8217;s counter wraps after 65535, but the scan uses a wider unsigned integer and can keep counting. &#8220;Unsigned&#8221; means it cannot be negative. Here, the important difference was what happened when one counter returned to zero and the other did not.</p><p>For SPE3 Gi2, the scan started at 65535 and the cached available counter was 1. In a 16-bit counter, those positions are only two steps apart:</p><pre><code><code>Scan position using the wider integer:  65535 &#8594; 65536 &#8594; 65537
The same position expressed in 16 bits: 65535 &#8594;     0 &#8594;     1
Cached available counter:                                  1
</code></code></pre><p>Follow the two rows in that example. After two buffers, the wider counter reaches 65537 while the cached counter is still 1. Comparing <code>65537 == 1</code> returns false, so the scan does not take the branch that refreshes the cached value from guest memory.</p><p>The next calculation uses 16-bit arithmetic, though. In that calculation, 65537 becomes 1, and the number of remaining entries comes out as zero. My model stopped the scan at that point, even though the guest had supplied more buffers.</p><p>That explains the otherwise odd byte counts. The model counted two 2060-byte buffers for Gi2, totaling 4120 bytes. For Gi3 it counted three, totaling 6180 bytes. Neither total could hold the jumbo frame being tested, despite the replenished buffers waiting in guest memory.</p><p>I changed the model&#8217;s comparison to use only the lower 16 bits of the scan position. Now 65537 becomes 1 for that comparison, the cached value gets refreshed, and the scan finds enough space. A small packet also passed because it fit in the buffers counted before the problem appeared. A control test whose counters did not cross 65535 passed as well.</p><p>This was still a model of the arithmetic. I had not tested a running QEMU device, and I did not deploy that experimental comparison change. I used it to narrow the source review before testing an existing upstream correction.</p><p>The running CML binary reported QEMU 8.2.2 from Ubuntu package <code>1:8.2.2+ds-0ubuntu1.18</code>. Its package patch directory had no changes to these functions.</p><p>I found <a href="https://github.com/qemu/qemu/commit/f937309fbdbb48c354220a3e7110c202ae4aa7fa">upstream commit </a><code>f937309</code>, an existing virtio-net receive-stall correction. It changes the insufficient-buffer notification and retry path. I used that correction for the compiled comparison, keeping it separate from my experimental counter change.</p><h3><strong>Testing the correction inside CML</strong></h3><p>I cloned the CML VM and disconnected all 18 copied lab interfaces so the test would stay separate from the running network. In that clone, I built the same Ubuntu QEMU source package twice: once unchanged, then with upstream commit <code>f937309</code> backported. A backport applies a fix to an older version without upgrading the whole application.</p><p>The installed Ubuntu binary did not include QEMU&#8217;s qtest accelerator. I enabled it in the source rebuild for the direct regression test. qtest is QEMU&#8217;s device-testing framework; it let me test the virtual NIC without booting an entire router for every attempt.</p><p>The compiled <code>rx-jumbo-wrap</code> test put an emulated receive queue into each of the three states I had observed. It then queued a jumbo frame, replenished the buffers and notified the device that more buffers were available.</p><p>The unchanged build stalled in all three cases. With the patch, the full frame arrived through three successive wraps in each case. I could now reproduce the failure without booting IOS, change the QEMU code, and run the same test again.</p><p>Next I booted the cloned SPE3 and SP1 on the patched executable, retaining their IOS XE configurations, virtio NICs, and router MTU 9216. Two isolated Linux endpoints sent traffic through both routers. Five 9000-byte DF pings passed. DF means &#8220;Don&#8217;t Fragment&#8221;: the packet must fit the path without being split into smaller IP fragments, making this a useful MTU check. A six-minute bidirectional TCP run delivered 360 MB in each direction at 8 Mbit/s. Counter sampling across the controls and load test recorded four wraps on each of the four traffic-facing NICs, with no receive stall and no IS-IS flap.</p><p>The long TCP test still recorded 15 retransmissions in one direction and 17 in the other. I looked at the live socket counters and found tiny 12-byte retransmissions with duplicate acknowledgments.</p><p>Notice the sizes: my test wrote 8960-byte blocks, but TCP negotiated an MSS of 8948. That left a 12-byte tail. I repeated the test with writes aligned to the negotiated MSS. It sustained the same rate for 30 seconds with zero retransmissions in either direction. The write pattern gave me a possible explanation for these short retransmissions, but it did not account for the earlier failures elsewhere in the lab.</p><p>The isolated router pair was my canary, a small deployment used to check a change before expanding it. The packet tests passed, so I could move on to live CML. This was still an experimental local backport, not a build supplied by Cisco.</p><h3><strong>Applying the backport to live CML</strong></h3><p>The routers were already on virtio, so there was no further NIC-model change to make. I needed them to use the patched QEMU executable on the CML host. An IOS <code>reload</code> would leave that QEMU process running. Each router needed a full CML stop/start.</p><p>I recorded the live QEMU package version, the executable hash, and the running domain definitions. The tested executable came from the isolated clone; I verified its SHA-256 again on the live host before installation.</p><p>The following is a record of the host change, not a standalone installation recipe. It depends on the exact tested binary, firmware checks, preserved configuration and rollback material. I preserved the packaged executable with a local diversion:</p><pre><code><code>dpkg-divert --local --rename --add \
  --divert /usr/bin/qemu-system-x86_64.distrib \
  /usr/bin/qemu-system-x86_64
</code></code></pre><p>I put the patched binary at the normal executable path. Routers that were already running kept their original mapped executable until stopped. To roll back, the script restores the diverted vendor binary, and the affected routers need another stop/start. When a suitable vendor package replaces this local build, the diversion also needs to be removed.</p><p>The first patched start on live CML failed before IOS could boot:</p><p><code>could not load PC BIOS 'bios-256k.bin'</code></p><p>Moving the executable to <code>/usr/bin</code> had changed where it looked for firmware. The packaged BIOS was already under <code>/usr/share/seabios</code>; I added the missing firmware links under <code>/usr/share/qemu</code> and recorded them for rollback. After checking the startup path, I retried. The fix here was in the host installation, so there was no reason to change the router&#8217;s MTU or IOS configuration.</p><p>After correcting the firmware lookup, SPE3 booted on the patched executable. I verified the hash of <code>/proc/&lt;pid&gt;/exe</code>, compared its operational configuration with the saved pre-restart configuration, and checked its control plane. Both IS-IS neighbors and both route-reflector sessions were up. Five 9216-byte DF pings to its SP1 neighbor passed:</p><pre><code><code>Sending 5, 9216-byte ICMP Echos to 10.0.0.16, timeout is 2 seconds:
Packet sent with the DF bit set
!!!!!
Success rate is 100 percent (5/5)
</code></code></pre><p>SPE3 was now running the tested binary with its saved configuration and routing sessions intact. I restarted the remaining routers in waves, keeping alternate core and route-reflector paths available. Before moving to the next wave, I checked routing, saved configuration and the executable actually running.</p><h3><strong>The receive stall was gone, but TCP still failed</strong></h3><p>Once all 13 routers were running the tested QEMU build, the network started looking healthy again. Their saved configurations were intact, every expected IS-IS neighbor relationship was up, and BGP peers had re-established their sessions. I could reach all 28 network devices. Large packets were getting through too: each of the six directions between the three datacenters passed five 9000-byte pings with fragmentation disabled.</p><p>I also went back to the receive-ring counters. During the first live traffic window, I recorded 18 rollovers across ten interfaces, including two that had stalled before. This time reception continued through the rollovers, and the IS-IS logs showed no new neighbor changes. The observation window was short, but it included the condition that had stopped delivery.</p><p>The applications began recovering as connectivity returned. All nine K3s nodes reported Ready, and the Grafana and VictoriaMetrics storage volumes reattached automatically. Grafana&#8217;s volume recovered all three replicas. VictoriaMetrics was accessible again, but its volume was still degraded while Longhorn rebuilt the missing redundancy.</p><p>The pings passed, but the next TCP test still failed. A 1 MiB transfer using small segments timed out at only 1 Mbit/s, with hundreds of retransmissions.</p><p>I reduced the transfer to 256 KiB and compared small and jumbo segments. The small-segment replay still failed, with 117 retransmissions recorded at one endpoint and 95 at the other. The jumbo replay moved the same amount of data in both directions in about 2.2 seconds without a single retransmission. I had requested an MSS of 1200 for the small test and 8960 for the jumbo test; the latter negotiated to 8948 bytes.</p><p>The QEMU stall was no longer appearing, but small-segment transfers were still failing. I stopped the planned sustained throughput test and captured the smaller transfer as it crossed the leaves.</p><h3><strong>Finding the second fault at the leaf</strong></h3><p>I captured the failing transfer at both K3s guests, Proxmox and EVE. To follow those captures, take a look at what EVE creates when I draw a connection between two virtual devices.</p><p>For the QEMU-based nodes in this lab, EVE connects virtual Ethernet ports through host-side TAP interfaces and bridges. A TAP interface is a software Ethernet endpoint: QEMU reads and writes Ethernet frames through it on behalf of a guest NIC. A bridge joins those endpoints into a Layer 2 network, much like a small Ethernet switch. A simple internal connection has this shape once the nodes are running:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZqWm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZqWm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 424w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 848w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 1272w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZqWm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png" width="1456" height="718" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:718,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:176498,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZqWm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 424w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 848w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 1272w, https://substackcdn.com/image/fetch/$s_!ZqWm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6151e09e-3bc7-4291-9aef-f442c5578f87_2100x1035.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 3a. The connection in the topology editor becomes a path through QEMU, TAP interfaces and a host bridge.</em></p><p>The <code>vunl</code> interfaces are the host-side endpoints, not the bridges themselves. An internal lab network uses a bridge named <code>vnet...</code> in this setup. An external Cloud connection attaches to a <code>pnet...</code> bridge, which also connects to an interface of the EVE host. That external path is how my K3s VMs on Proxmox reach the EVE-hosted leaves. The <a href="https://github.com/byrn-baker/blog-sandbox/blob/main/demo-lab/08-hypervisor-interconnect.md">lab interconnect guide</a> records that wiring.</p><p>Here is the mapping for DCA-Leaf01, EVE node ID 3. Its host interface <code>vunl0_3_5</code> backs Ethernet5, and <code>vunl0_3_2</code> backs Ethernet2. The final number is the interface index in this node&#8217;s mapping. Inside vEOS, the corresponding Linux NICs are <code>vmnicet5</code> and <code>vmnicet2</code>.</p><p>These are names for the two sides of the same VM connection: <code>vunl...</code> on the EVE host and <code>vmnicet...</code> inside the switch. I needed the host-side names for my captures and the guest-side names when changing offload settings.</p><p>The <a href="https://github.com/byrn-baker/blog-sandbox/blob/main/specs/blog-obs-stack/specs/001-otel-network-telemetry/evidence/2026-09-11-longhorn-recovery/qemu-live/capture-dca-dcb/eve-links.json">saved EVE interface map</a> shows Ethernet5&#8217;s TAP attached to <code>pnet11</code>, alongside the EVE host&#8217;s <code>eth11</code>. Ethernet2&#8217;s TAP belonged to <code>vnet0_11</code>, alongside <code>vunl0_2_1</code>, the peer spine&#8217;s TAP. For the traffic direction I was investigating, the path was:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!f1oF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!f1oF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 424w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 848w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 1272w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!f1oF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png" width="1456" height="838" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:838,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:253374,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!f1oF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 424w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 848w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 1272w, https://substackcdn.com/image/fetch/$s_!f1oF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a2953a7-40b1-4d4c-82c8-9e1d0cc8d4c7_2100x1208.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 3b. I captured traffic at two host-side TAPs around the same leaf. The labels map each capture to its guest port and bridge.</em></p><p>I was capturing two ports on DCA-Leaf01. At <code>vunl0_3_5</code>, I could see the packet arriving at the server-facing Ethernet5 port. At <code>vunl0_3_2</code>, I could see what left Ethernet2 toward the fabric. The outgoing packet had a VXLAN wrapper, so I compared the TCP packet inside it with the one that had entered Ethernet5.</p><p>On the EVE host, these read-only commands expose the relevant bridge membership:</p><pre><code><code>bridge link show dev vunl0_3_5
bridge link show dev vunl0_3_2
ls /sys/class/net/pnet11/brif
ls /sys/class/net/vnet0_11/brif
</code></code></pre><p>Look for <code>master</code> in the <code>bridge link</code> output. That tells you which bridge owns the interface. The <code>brif</code> directory lists the other interfaces attached to that bridge. The names above belong to this lab instance, so check the mapping before using them as capture points in another lab.</p><p>Now look at the sequence numbers in the capture. A TCP sequence number identifies where a segment&#8217;s data starts in the stream. The second segment starts 1188 bytes after the first, exactly the length of the first payload.</p><p>After the leaf forwarded them, those two 1188-byte payloads had become one 2376-byte payload starting at the original sequence number. Combining segments is not automatically a fault. The problem was that the combined packet had an invalid checksum, the integrity check covering the TCP header and data.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T-bK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 424w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 848w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 1272w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T-bK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png" width="1281" height="235" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:235,&quot;width&quot;:1281,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40900,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!T-bK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 424w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 848w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 1272w, https://substackcdn.com/image/fetch/$s_!T-bK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7873175f-10f6-4e3c-b8fe-0af5b293fe9c_1281x235.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>I had used a known repeated byte for the test payload, which let me reconstruct the payload and verify its checksum against the captured headers. The invalid combined segment also appeared at the receiving host. About two seconds later, valid 1188-byte retransmissions arrived, and the receiver acknowledged those bytes.</p><p>That last check matters with offload captures. A checksum can look wrong in a sender-side capture before the NIC finishes processing it. Here, I could follow the malformed segment to the receiving host and then see the valid retransmissions arrive.</p><p>Both source leaf ingress NICs had <code>generic-receive-offload: on</code>: DCA-Leaf01 <code>vmnicet5</code> and DCB-Leaf01 <code>vmnicet4</code>. Generic Receive Offload, or <a href="https://docs.kernel.org/networking/segmentation-offloads.html#generic-receive-offload">GRO</a>, combines received segments to reduce processing work. Segmentation and checksum handling later in the path must still produce valid packets.</p><p>I had valid packets arriving at the leaf and an invalid combined packet leaving it. Disabling GRO on those two ingress NICs gave me a specific change to test against that capture.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2QXN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2QXN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 424w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 848w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 1272w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2QXN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png" width="1456" height="718" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/da1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:718,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:248703,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2QXN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 424w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 848w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 1272w, https://substackcdn.com/image/fetch/$s_!2QXN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fda1af698-f84d-4825-b049-8dbcae8c1ba7_2100x1035.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 4. I compared the packet before and after the leaf. The lower row previews the controlled test described next: preserving the path was essential to testing the workaround.</em></p><p>The <a href="https://github.com/byrn-baker/blog-sandbox/tree/main/specs/blog-obs-stack/specs/001-otel-network-telemetry/evidence/2026-09-11-longhorn-recovery/qemu-live/capture-gro-off-pinned">packet captures and parsed results</a> are available in the repository.</p><h3><strong>Testing GRO on the actual TCP path</strong></h3><p>I temporarily disabled GRO on DCA-Leaf01 <code>vmnicet5</code> and DCB-Leaf01 <code>vmnicet4</code>, checking each setting before and after:</p><pre><code><code># On DCA-Leaf01
bash sudo ethtool -K vmnicet5 gro off
bash ethtool -k vmnicet5

# On DCB-Leaf01
bash sudo ethtool -K vmnicet4 gro off
bash ethtool -k vmnicet4
</code></code></pre><p>I ran the replay again and it still failed, with 136/140 retransmissions. Then I checked the captures. The new connection had entered Leaf02 at both sites. I had changed Leaf01.</p><p>The K3s hosts use bonds, which group multiple NICs into one logical interface. Their <code>balance-xor</code> mode uses a <code>layer3+4</code> hash: IP addresses and transport ports determine which member carries a connection. Changing the TCP source port can therefore send a replay through another leaf even when its destination stays the same.</p><p>That is what happened here. The small-segment replay bypassed both modified ingress ports. Its paired jumbo replay passed with zero retransmissions, but neither result tested the change I had made on Leaf01.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0Y1Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 424w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 848w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png" width="1456" height="713" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:713,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:259200,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 424w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 848w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 1272w, https://substackcdn.com/image/fetch/$s_!0Y1Y!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe06a6c37-c150-44cf-94a7-2f93af3b719a_2100x1028.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><em>Figure 5. I had changed Leaf01, but the first replay used Leaf02. Fixing the ports and checking the captures put the comparison back on the modified path. One end is shown; the same issue occurred at both sites.</em></p><p>I repeated the small-segment test with the original source port, 38869, and destination port 49492. Captures confirmed the original ingress path through DCA-Leaf01 and DCB-Leaf01. This time, 256 KiB transferred in each direction with matching SHA-256 hashes and zero retransmissions. Both directions completed within about 2.5 seconds.</p><div class="captioned-image-container"><figure><div class="image-link image2" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ABTk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 424w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 848w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 1272w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ABTk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png" width="1155" height="162" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:162,&quot;width&quot;:1155,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:23895,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/215447783?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ABTk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 424w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 848w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 1272w, https://substackcdn.com/image/fetch/$s_!ABTk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4e173d33-9913-49a3-80ae-d2b421cc718a_1155x162.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></div></figure></div><p>I checked the captures again. Each source leaf received 221 valid data segments and emitted 221 valid inner TCP segments in VXLAN. The malformed combined segments from the baseline were gone at those capture points.</p><p>Turning GRO off worked on this path. I had not located the faulty code inside vEOS, and I still needed to test the other leaves and workloads.</p><p>After that temporary test, I put GRO back to <code>on</code> on both ports and checked the readback. The QEMU correction stayed deployed. I still needed to generate a persistent GRO configuration for the leaves and check the other paths.</p><h3><strong>Rolling the correction across the leaves</strong></h3><p>I needed the setting to survive beyond a shell command. I added a device context listing the ten modeled Ethernet data NICs on each of the nine leaves, then generated an EOS event handler for each NIC. An event handler is a saved action triggered by an event, in this case startup.</p><p>The source is <code>golden-config/templates/eos/platform.j2</code>, which reads the <code>lab_gro_workaround</code> context. I published <a href="https://github.com/byrn-baker/blog-sandbox/commit/d3fdafe">the correction</a> after 82 template/structure tests and 149 Batfish/config checks passed. Here is the handler for <code>vmnicet5</code>:</p><pre><code><code>event-handler lab-gro-off-vmnicet5
   action bash sudo -n /sbin/ethtool -K vmnicet5 gro off
   trigger on-boot
   delay 60
</code></code></pre><p>EOS runs the on-boot handler after configuration as well as at boot. That gave me two things to inspect: <code>ethtool -k</code> for the current Linux offload state, and startup configuration for the saved instruction. I checked both during the rollout. I did not cold-boot the leaves.</p><p>DCA-Leaf01 passed first. I then deployed to the remaining leaves in two waves, comparing each plan with Nautobot&#8217;s fresh intended configuration. On each leaf I checked the current offload settings and the saved startup handlers.</p><p>On DCC-Leaf02, GRO was already off on all ten NICs, but only nine startup handlers had been saved. The switch was in the state I wanted at that moment; one interface was missing the instruction intended to restore that state after a reboot. Netmiko, the library sending the configuration commands, had reported <code>Pattern not detected: 'end' in output</code>. With Fail Job on Task Failure enabled, that incomplete deployment remained visible as a failure.</p><p>I read DCC-Leaf02 again and built a recovery plan containing only the missing handler. After deploying it, I read back all ten saved handlers. The fleet then had GRO off on all 90 data NICs, with a saved handler for each. An actual cold boot was still needed to test their startup execution.</p><p>I repeated the small-segment transfer and checked the leaf captures. The TCP checksums were valid at the observed boundaries, and the malformed combined segments were gone from that replay. Routing was up across the fabric. The transfer completed with matching data, zero retransmissions at one endpoint and one at the other.</p><p>I widened the test to include all nine K3s hosts and every site pair. All nine bidirectional transfers completed, but their endpoints recorded 41 retransmissions in total. A separate three-minute transfer also completed, with two retransmissions at one end and eight at the other. Packet delivery had improved enough to finish these transfers; it had not become consistently free of retransmissions. With Longhorn already rebuilding a volume, I held off on adding a higher-rate traffic test.</p><p>K3s showed all nine nodes and 65 pods Ready in the recorded samples. The first 23-minute comparison had no increase in container restarts. Later, four of Longhorn&#8217;s storage-integration containers restarted, so I went back to the volume that was rebuilding.</p><p>The clearest remaining failure came near the end of a VictoriaMetrics replica rebuild. It reached 97%, then stopped with a read/write timeout and connection resets. Longhorn automatically replaced the failed replica and tried again, but another attempt reset too. Grafana&#8217;s storage remained healthy. The GRO workaround had corrected the malformed packets in my captures, yet it had not made this storage transfer reliable.</p><p>CE2 and CE3 were carrying roughly 17&#8211;18 Mbit/s against a configured 20 Mbit/s throughput limit, so I checked whether the rebuild was running out of forwarding capacity. The sampled forwarding and queue counters did not show that limit causing drops. The endpoints did not show CPU or memory exhaustion either. I could not explain the resets from those checks, so I moved on to the connection Longhorn was losing.</p><h3><strong>Testing how quickly the rebuild connection gave up</strong></h3><p>The rebuild was getting almost to the end before its connection reset. Longhorn coordinates that work through remote procedure calls, or RPCs, between storage processes. I wanted to see how long the TCP connection carrying those calls would wait when packets stopped getting through.</p><p>I inspected the sockets owned by the live rebuild processes. They had <code>TCP_USER_TIMEOUT</code> set to 20 seconds, with TCP keepalive enabled after 15 seconds of inactivity and a 15-second interval between unanswered probes. I read those values from the running connections instead of assuming the chart defaults were in use.</p><p><code>TCP_USER_TIMEOUT</code> limits how long TCP will tolerate data remaining unacknowledged, or buffered without being sent. It also affects when an idle connection with unanswered keepalive probes is closed. The interaction mattered here: a short interruption could miss a probe, and restoring the network would not necessarily rescue the connection before its timeout logic ran.</p><p>I tested that possibility in an isolated Linux network namespace, which gave me a separate network environment where I could deliberately drop packets. I introduced a five-second loss window covering the first keepalive probe, then let traffic through again. I ran the same experiment with a 20-second user timeout and a 60-second user timeout, changing only that setting.</p><p>By the check at about 35 seconds, the 20-second connection had reported <code>ETIMEDOUT</code>, Linux&#8217;s connection-timeout error. The 60-second connection was still open and could exchange data after the interruption. The loss window was the same; the longer timeout gave that test connection enough time to recover.</p><p>I had tested one connection under a controlled loss window. That did not reproduce a whole Longhorn rebuild or tell me what was dropping packets in the lab.</p><p>I raised the user timeout on specific live rebuild RPC sockets as a temporary mitigation. Those changes lasted only as long as the connections stayed open; they were not saved Longhorn settings.</p><p>Both application volumes eventually recovered three read/write replicas. I could confirm that they were healthy again, but I could not say the timeout change alone had done it. I still needed to find the cause of the interruptions and decide whether a permanent Longhorn timeout change was appropriate.</p><h2><strong>Could I trust the next successful test?</strong></h2><p>Once Longhorn finished rebuilding, I ran the small-segment and jumbo checks again. A reboot had already given me a temporary recovery earlier in this investigation, so I wanted to repeat the transfers that had exposed the failures.</p><p>Before starting, I checked the running QEMU binaries and the leaf offload settings. All 13 router processes matched the tested build, and all 90 leaf data NICs still had GRO off. Both CML socket samples had clear receive queues. Routing was up, both application volumes had three healthy replicas, and the cluster snapshots showed no pod replacements or increase in container restarts.</p><p>The small-segment transfers completed across nine host pairs, covering every K3s node and every site pair. The data matched at both ends, with zero retransmissions. Jumbo transfers across three site pairs also completed with matching data and zero retransmissions. Large pings passed in all six inter-site directions.</p><p>I then kept a jumbo transfer running for three minutes at 1 Mbit/s in each direction. It delivered just over 22.5 million bytes each way, with matching hashes. One endpoint recorded no retransmissions; the other recorded four. The original receive freeze had not returned, and the small-segment tests were working, but the longer run still gave me a reason to keep looking. None of these deliberately paced tests established the network&#8217;s maximum capacity or replaced a cold-boot check of the leaf handlers.</p><h2><strong>The remaining problem showed up under load</strong></h2><p>The short transfers were working. Then I looked at a cross-site Longhorn connection under storage load: roughly 3 MiB retransmitted out of 31 MiB sent, with a reported round-trip time around 734 ms. I still had a problem to investigate.</p><p>The routing logs supplied another clue. Before the repeat tests, BFD had triggered BGP session resets, including one between DCA-Spine01 and DCB-Spine02. BFD is a rapid failure detector: if its probes stop getting through in time, it can tell BGP to tear down a session. The most recent session recovered in roughly two seconds, but the message <code>Cease/BFD down &lt;Hard Reset&gt;</code> did not tell me why the probes had been missed. Packet loss and scheduling delays were possibilities to investigate, not conclusions from that log alone.</p><p>The QEMU and GRO changes stayed in place because their failure cases were now passing. I still needed to correlate Longhorn traffic with the BFD resets and run a sustained load test. The <a href="https://github.com/byrn-baker/blog-sandbox/tree/main/specs/blog-obs-stack/specs/001-otel-network-telemetry/evidence/2026-09-12-server-renumber">saved verification evidence</a> contains the process checks and traffic samples for that work.</p><h2><strong>Where I would start next time</strong></h2><p>I would start with the failing connection and map its actual path. On CML, I have the router&#8217;s interface counters, the host socket queues and QEMU&#8217;s receive-ring state to compare. In EVE, I can capture the packet before and after the leaf and check that it leaves with the same valid TCP data.</p><p>I also need to keep the test addresses and ports fixed. Changing the source port sent the first GRO replay through another leaf. It looked like the setting had done nothing until I checked which interfaces the new flow was actually using.</p><p>The lab was working well enough to finish the short transfers and recover storage, but the load-related retransmissions and routing resets still needed attention. In <a href="https://vscode-remote+ssh-002dremote-002b192-002e168-002e3-002e21.vscode-resource.vscode-cdn.net/home/ubuntu/demo-blog/part7-argocd-observability-snmp.md">From Network Devices to Grafana: Building the Lab&#8217;s SNMP Pipeline</a>, I bring the network&#8217;s interface counters into Grafana so I can follow those measurements while the applications use the network. That post also covers the event-collection gap: polling counters will not capture every brief BFD or BGP transition.</p><h2><strong>References</strong></h2><ul><li><p><a href="https://github.com/qemu/qemu/commit/f937309fbdbb48c354220a3e7110c202ae4aa7fa">QEMU receive-stall correction, f937309</a></p></li><li><p><a href="https://docs.kernel.org/networking/segmentation-offloads.html#generic-receive-offload">Linux segmentation offloads and GRO</a></p></li><li><p><a href="https://github.com/byrn-baker/blog-sandbox/commit/d3fdafe">Persistent leaf GRO source</a></p></li><li><p><a href="https://man7.org/linux/man-pages/man7/tcp.7.html">Linux TCP socket options</a></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">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[Nine nodes. Zero host files]]></title><description><![CDATA[Part 5 left a single Ubuntu VM on VLAN 100 with an IP, a bond, and a route to the internet. That was the proof the fabric had an exit. This post turns that one server into nine, stands up a Kubernetes]]></description><link>https://www.byrnbaker.me/p/one-cluster-three-datacenters-zero</link><guid isPermaLink="false">https://www.byrnbaker.me/p/one-cluster-three-datacenters-zero</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 10 Sep 2026 13:01:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IDiX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.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_!IDiX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IDiX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IDiX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98610,&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/214441608?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.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_!IDiX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!IDiX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8af89c6b-0f9c-424f-a8fb-397fda11e516_1500x1000.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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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;ae9d6267-78bb-4547-a3ff-5a8bc9c090c5&quot;,&quot;duration&quot;:null}"></div><h2><span>What Kubernetes is, and why K3s</span></h2><p>Quick grounding for anyone who hasn&#8217;t touched this before. Kubernetes runs containers across a group of machines and keeps them running the way you described, not the way they happen to be right now. You tell it &#8220;I want this app running, with this much memory, reachable on this port,&#8221; and a control plane continuously checks the real state against that description and corrects any gap: a crashed container gets restarted, a machine that goes away gets its work moved somewhere else. The machines are called nodes. The smallest thing Kubernetes schedules is a pod, one or more containers that share networking and storage. A cluster is the whole assembly: the API server and scheduler that make the decisions, plus every node that runs the actual work.</p><p>K3s is a real Kubernetes, not a scaled-down imitation of one: same API, same <code>kubectl</code>, same Helm charts you&#8217;d point at a full-size EKS or GKE cluster. What&#8217;s different is the packaging. Every control-plane component (API server, scheduler, controller-manager) runs out of a single binary instead of a dozen separate processes, and the default datastore is sqlite3 rather than etcd, though etcd is still available for exactly the case this cluster needs: a real multi-node HA control plane. It&#8217;s built for edge, homelab, IoT, and other places a full Kubernetes install would be overkill, and that&#8217;s the profile these VMs fit: modest CPU and RAM, sitting behind emulated network gear, on a fabric that&#8217;s slow and jittery on purpose.</p><p>The rule for this series has been that Nautobot is the source of truth and Git holds the intent behind it. The network devices already work that way through Golden Config. The servers didn&#8217;t, until now. By the end of this post the servers are provisioned by Ansible, and Ansible reads everything it needs, which hosts exist, how to reach them, what role each one plays, straight from the model.</p><p>One nuance this post makes concrete, because glossing over it is how &#8220;source of truth&#8221; turns into a lie: Git holding the intent and the running Nautobot reflecting it are two separate things. Some data (config contexts) Nautobot pulls from Git on every sync, so a commit is enough. Other data (anything Design Builder generates) is written once at build time and then lives only in the database, so a commit alone never touches it. When the two drift, you fix Git for the next build and you apply the change to the running instance as a deliberate second step. You will see both halves in this post.</p><h2><span>The shape of the thing we&#8217;re building</span></h2><p>Ten Ubuntu VMs, all already modeled in Nautobot as devices (Part 1 built them, Part 5 cabled the first one&#8217;s bond). Nine become a single K3s cluster. The tenth, DCA-DNS, becomes a BIND server.</p><p>The cluster is stretched, not sharded. One cluster spans DC-A, DC-B, and DC-C. That only works because VLAN 100 is one EVPN type-5 segment stretched across all three sites, so every node shares the 192.168.100.0/24 subnet no matter which datacenter it physically sits in. Three server nodes run embedded etcd and form the control plane, and all three live in DC-A. The other six nodes are agents, split evenly across DC-B and DC-C.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IxuC!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IxuC!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 424w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 848w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 1272w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IxuC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png" width="696" height="522" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:522,&quot;width&quot;:696,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63679,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.byrnbaker.me/i/214441608?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IxuC!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 424w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 848w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 1272w, https://substackcdn.com/image/fetch/$s_!IxuC!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c195bf9-f21a-44f4-b152-0a0586d546cd_696x522.png 1456w" sizes="100vw" loading="lazy"></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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>DCC-k3s-w6 came later than the other eight, added specifically so DC-C would have three workers instead of two. That matters once storage enters the picture below: three nodes per DC means every DC can hold a full 3-replica Longhorn volume without a single replica crossing the emulated core.</p><p>Keeping all three etcd members in one DC is not where this started. The first design put one member in each site, so the cluster would survive losing a whole datacenter. That&#8217;s the textbook layout, and on this fabric it does not work. The why turned out to be the most useful thing in this post, so we&#8217;ll come back to it once the cluster is standing.</p>
      <p>
          <a href="https://www.byrnbaker.me/p/one-cluster-three-datacenters-zero">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Servers VRF Needed a Way Out. That Meant a Border Leaf.]]></title><description><![CDATA[One leaf per site, one NAT edge, and a /32 that saved the return path]]></description><link>https://www.byrnbaker.me/p/the-servers-vrf-needed-a-way-out</link><guid isPermaLink="false">https://www.byrnbaker.me/p/the-servers-vrf-needed-a-way-out</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 03 Sep 2026 13:03:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!x3tD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_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_!x3tD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!x3tD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!x3tD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg" width="728" height="488.6575342465753" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b7a73cfa-d158-4c8c-a008-db6c2de35cdf_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;:728,&quot;bytes&quot;:218442,&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/213359064?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_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_!x3tD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 424w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 848w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_1168x784.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!x3tD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb7a73cfa-d158-4c8c-a008-db6c2de35cdf_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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>Part 4 built the datacenter fabric and proved EVPN worked across all three sites. The DCI overlay exchanged type-2 MAC routes between DC-A, DC-B, and DC-C, so a K3s node on a leaf in one datacenter could reach a node on a leaf in another, and a ping across the stretched VXLAN data plane came back with zero loss. What Part 4 did not build was a way for that <code>SERVERS</code> traffic to leave the fabric. The servers could reach each other across every DC, but they had no path to the internet.</p><p>This post builds that exit. By the end, a server in any of the three DCs can reach the internet through <code>BORDER1</code>, and the return traffic finds its way back.</p><p><br></p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;f515ebcb-93c2-4e9b-b912-710fc8e2a25c&quot;,&quot;duration&quot;:null}"></div>
      <p>
          <a href="https://www.byrnbaker.me/p/the-servers-vrf-needed-a-way-out">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[15 Arista Switches. Intra-DC EVPN Is Up]]></title><description><![CDATA[I hit Start All. Fifteen vEOS nodes. Half came up empty.]]></description><link>https://www.byrnbaker.me/p/15-arista-switches-one-start-button</link><guid isPermaLink="false">https://www.byrnbaker.me/p/15-arista-switches-one-start-button</guid><dc:creator><![CDATA[Byrn Baker]]></dc:creator><pubDate>Thu, 27 Aug 2026 13:03:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pwaS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I hit &#8220;Start All Nodes&#8221; in EVE-NG. Fifteen Arista vEOS nodes began decompressing at the same time. Half the nodes didn&#8217;t fully boot and required that I stop them, wipe them, and restart them.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pwaS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pwaS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pwaS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:122906,&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/212242390?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.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_!pwaS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!pwaS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9fcd70b-3d8d-4bbb-8aca-a7fbf9ed86c2_1500x1000.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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>That&#8217;s the datacenter fabric for this lab: 15 vEOS switches across three sites, eBGP underlay, EVPN overlay, all running inside EVE-NG on the same Proxmox host as the CML SP core. Getting them configured was the easy part (Nautobot templates handle that). Getting them to actually boot was the first real problem.</p><p>Each box gets a startup-config in EVE-NG so it comes up reachable. Production config is a Golden Config push from Nautobot.</p><p>If you want to reproduce this, you&#8217;ll need two VMs on a Proxmox host (or any KVM hypervisor). CML needs 12 vCPU, 80 GB RAM, and 80 GB disk for the 13 Cisco cat8000v nodes. EVE-NG needs 8 vCPU, 64 GB RAM, and 80 GB disk for the 15 Arista vEOS nodes. That&#8217;s roughly 144 GB total RAM for the network layer alone. You&#8217;ll need the EVE-NG VM running with the Blog-SandBox topology loaded.</p><p>Here&#8217;s the proof. Local leaves Established. Remote spines Active:</p>
      <p>
          <a href="https://www.byrnbaker.me/p/15-arista-switches-one-start-button">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>