<?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"><channel><title><![CDATA[NetDev]]></title><description><![CDATA[NetDev]]></description><link>https://next-hop.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 18 Sep 2026 01:19:44 GMT</lastBuildDate><atom:link href="https://next-hop.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Learning Byte 001 — Junos Pyez Foundations]]></title><description><![CDATA[Scope
• Establish NETCONF/SSH connectivity to Junos using PyEZ and verify access.• Use dev.facts as the primary data source: dump full facts, then produce a five-field summary (Hostname, Model, Serial, Version, Uptime).• Scale the exact same logic fr...]]></description><link>https://next-hop.hashnode.dev/learning-byte-001-junos-pyez-connect-and-read-facts</link><guid isPermaLink="true">https://next-hop.hashnode.dev/learning-byte-001-junos-pyez-connect-and-read-facts</guid><category><![CDATA[Pyez]]></category><category><![CDATA[Python]]></category><category><![CDATA[Network Automation]]></category><category><![CDATA[Juniper]]></category><dc:creator><![CDATA[config]]></dc:creator><pubDate>Fri, 05 Sep 2025 03:59:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/udSClwyJF9Y/upload/555cc2d1610df64064a717d7912830c0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-scope">Scope</h3>
<p>• Establish NETCONF/SSH connectivity to Junos using PyEZ and verify access.<br />• Use <code>dev.facts</code> as the primary data source: dump full facts, then produce a five-field summary (Hostname, Model, Serial, Version, Uptime).<br />• Scale the exact same logic from one device → a list of devices (sequential) → concurrent collection with a small thread pool.<br />• Introduce a concise PyEZ mental model (NETCONF over SSH, <code>dev.facts</code> dict vs. <code>dev.rpc</code> XML) without deep XML parsing.</p>
<h3 id="heading-network-topology">Network Topology</h3>
<ul>
<li><p>Devices: vEX-01…vEX-04 in EVE-NG</p>
</li>
<li><p>Management: ge-0/0/8 on 10.30.0.0/24, gateway 10.30.0.1</p>
</li>
<li><p>Access: SSH enabled, NETCONF enabled with “set system services netconf ssh”</p>
</li>
<li><p>Controller: Python 3.9+, PyEZ installed with “pip install jnpr.junos”</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757130356874/a98db7ed-2d34-4e92-9396-ba3d7beeb30c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-what-is-pyez">What is PyEZ</h3>
<ul>
<li><p><strong>What it is:</strong> A Python SDK that talks <strong>NETCONF over SSH</strong> to Junos and returns <strong>structured data</strong> (no CLI scraping).</p>
</li>
<li><p><strong>Two paths used:</strong> <code>dev.facts</code> (<strong>cached dict</strong> of common facts) and <code>dev.rpc.*</code> (<strong>direct RPCs</strong> that return XML).</p>
</li>
<li><p><strong>RPC mapping:</strong> XML tags map to Python methods (hyphen → underscore), e.g., <code>&lt;get-system-uptime-information&gt;</code> → <code>dev.rpc.get_system_uptime_information()</code>; confirm via <code>| display xml rpc</code>.</p>
</li>
<li><p><strong>Session model:</strong> Create a <code>Device</code>, <strong>open/close</strong> NETCONF (or use <code>with Device(...)</code>); reads are <strong>idempotent</strong> and safe to repeat.</p>
</li>
<li><p><strong>Why PyEZ:</strong> Consistent across platforms, structured-by-design, easy to serialize/log, and friendly to <strong>concurrency</strong> for I/O-bound work.</p>
</li>
</ul>
<h3 id="heading-quick-notes">Quick Notes</h3>
<ul>
<li><p>“with Device(...):” is a context manager; it opens NETCONF and always closes it on exit.</p>
</li>
<li><p>Dictionaries are key/value; .get() lets me provide safe defaults when a key isn’t present.</p>
</li>
<li><p>Exceptions happen a lot with networks (timeouts, auth). Keep try/except simple.</p>
</li>
<li><p>Serialization: json.dumps(obj, indent=2) turns Python data into readable text for logs and files.</p>
</li>
</ul>
<h2 id="heading-standard-section-part-1-devfacts-json-dump">STANDARD SECTION: PART 1 — dev.facts json dump</h2>
<p>Purpose: open NETCONF, read <code>dev.facts</code>, print it. This is the very first check.</p>
<h3 id="heading-code-lb001devfactsdumppy">CODE - lb001_dev_facts_dump.py</h3>
<pre><code class="lang-python"><span class="hljs-comment"># lb001_dev_facts_dump.py</span>
<span class="hljs-keyword">from</span> jnpr.junos <span class="hljs-keyword">import</span> Device
<span class="hljs-keyword">import</span> json

HOST = <span class="hljs-string">"10.30.0.241"</span>       <span class="hljs-comment"># edit for lab</span>
USERNAME = <span class="hljs-string">"admin"</span>
PASSWORD = <span class="hljs-string">"yourpassword"</span>

<span class="hljs-keyword">with</span> Device(host=HOST, user=USERNAME, passwd=PASSWORD) <span class="hljs-keyword">as</span> dev:
    facts = dict(dev.facts)  <span class="hljs-comment"># FactCache -&gt; plain dict</span>
    print(json.dumps(facts, indent=<span class="hljs-number">2</span>, default=str))
</code></pre>
<h3 id="heading-output">Output:</h3>
<pre><code class="lang-plaintext">{
  "current_re": [
    "re0",
    "master",
    "node",
    "fwdd",
    "member",
    "pfem"
  ],
  "domain": null,
  "fqdn": "vEX-01",
  "switch_style": "VLAN_L2NG",
  "HOME": "/root",
  "srx_cluster": null,
  "srx_cluster_id": null,
  "srx_cluster_redundancy_group": null,
  "RE_hw_mi": false,
  "serialnumber": "VM68BB99A1F4",
  "2RE": false,
  "master": "RE0",
  "RE0": {
    "mastership_state": "master",
    "status": "OK",
    "model": "RE-VMX",
    "last_reboot_reason": "Router rebooted after a normal shutdown.",
    "up_time": "1 hour, 45 minutes, 17 seconds"
  },
</code></pre>
<h3 id="heading-notes">NOTES</h3>
<p>• Transport &amp; parsing: NETCONF (XML) runs inside SSH; PyEZ receives XML, parses it, and gives me Python objects.<br />• First stop: dev.facts is a regular Python dict (not XML). I can print it as JSON for readability.<br />• Session lifecycle: <code>with Device(host, user, passwd) as dev:</code> opens NETCONF and always closes it on exit.<br />• Safe printing: <code>json.dumps(obj, indent=2, default=str)</code> (avoids type errors and keeps output readable).<br />• Keep errors simple at first; if something fails I want the exception to show while I lab.<br />• Reads are idempotent: facts/RPC “get” calls don’t change the box; safe to run repeatedly.</p>
<h3 id="heading-about-xmlrpcs">ABOUT XML/RPCS</h3>
<p>Junos exposes data as RPCs that return XML. PyEZ calls several of these behind the scenes and builds <code>dev.facts</code>. If/when I need to see the source, I’ll call the specific RPC (e.g., uptime or chassis inventory) and pretty-print the XML</p>
<h2 id="heading-standard-section-part-2-extract-a-minimal-subset">STANDARD SECTION: PART 2 — EXTRACT A MINIMAL SUBSET</h2>
<h3 id="heading-task">Task</h3>
<p>Produce a five-field summary from <code>dev.facts</code>: <strong>Hostname, Model, Serial, Version, Uptime</strong>.</p>
<h3 id="heading-inputs">Inputs</h3>
<ul>
<li><p>One reachable Junos device (NETCONF over SSH enabled).</p>
</li>
<li><p>IP/credentials.</p>
</li>
</ul>
<h3 id="heading-procedure">Procedure</h3>
<ol>
<li><p>Open a NETCONF session with <code>Device(...)</code>.</p>
</li>
<li><p>Read <code>dev.facts</code> and convert to a plain dict.</p>
</li>
<li><p>Build a tiny summary with simple fallbacks (<code>serialnumber|serial</code>, <code>RE0.up_time|uptime</code>).</p>
</li>
<li><p>Print results in a clean, predictable format.</p>
</li>
</ol>
<h3 id="heading-code-lb001factssubsetpy">CODE - lb001_facts_subset.py</h3>
<pre><code class="lang-python"><span class="hljs-comment"># lb001_facts_subset.py</span>
<span class="hljs-comment"># Purpose: connect to one Junos device with PyEZ and print five useful facts.</span>
<span class="hljs-comment"># Usage:</span>
<span class="hljs-comment">#   1) pip install jnpr.junos</span>
<span class="hljs-comment">#   2) Edit HOST, USERNAME, PASSWORD</span>
<span class="hljs-comment">#   3) python lb001_facts_subset.py</span>

<span class="hljs-keyword">from</span> jnpr.junos <span class="hljs-keyword">import</span> Device

<span class="hljs-comment"># --- edit for your lab ---</span>
HOST = <span class="hljs-string">"10.30.0.241"</span>
USERNAME = <span class="hljs-string">"admin"</span>
PASSWORD = <span class="hljs-string">"yourpassword"</span>

<span class="hljs-keyword">try</span>:
    <span class="hljs-comment"># Open NETCONF; session auto-closes when the block ends</span>
    <span class="hljs-keyword">with</span> Device(host=HOST, user=USERNAME, passwd=PASSWORD) <span class="hljs-keyword">as</span> dev:
        f = dict(dev.facts)  <span class="hljs-comment"># make it a plain dict for easy access</span>

        <span class="hljs-comment"># Build a tiny summary using simple fallbacks where platforms differ</span>
        hostname = f.get(<span class="hljs-string">"hostname"</span>, <span class="hljs-string">"N/A"</span>)
        model    = f.get(<span class="hljs-string">"model"</span>, <span class="hljs-string">"N/A"</span>)
        serial   = f.get(<span class="hljs-string">"serialnumber"</span>) <span class="hljs-keyword">or</span> f.get(<span class="hljs-string">"serial"</span>, <span class="hljs-string">"N/A"</span>)
        version  = f.get(<span class="hljs-string">"version"</span>, <span class="hljs-string">"N/A"</span>)
        uptime   = (f.get(<span class="hljs-string">"RE0"</span>, {}) <span class="hljs-keyword">or</span> {}).get(<span class="hljs-string">"up_time"</span>) <span class="hljs-keyword">or</span> f.get(<span class="hljs-string">"uptime"</span>, <span class="hljs-string">"N/A"</span>)

        print(<span class="hljs-string">f"\nDevice: <span class="hljs-subst">{HOST}</span>"</span>)
        print(<span class="hljs-string">f"  Hostname : <span class="hljs-subst">{hostname}</span>"</span>)
        print(<span class="hljs-string">f"  Model    : <span class="hljs-subst">{model}</span>"</span>)
        print(<span class="hljs-string">f"  Serial   : <span class="hljs-subst">{serial}</span>"</span>)
        print(<span class="hljs-string">f"  Version  : <span class="hljs-subst">{version}</span>"</span>)
        print(<span class="hljs-string">f"  Uptime   : <span class="hljs-subst">{uptime}</span>"</span>)

<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
    <span class="hljs-comment"># Keep errors straightforward in the first lesson</span>
    print(<span class="hljs-string">f"[error] <span class="hljs-subst">{HOST}</span>: <span class="hljs-subst">{e}</span>"</span>)
</code></pre>
<h3 id="heading-expected-output-shape">Expected Output (shape)</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">Device:</span> <span class="hljs-number">10.30</span><span class="hljs-number">.0</span><span class="hljs-number">.241</span>
  <span class="hljs-attr">Hostname :</span> <span class="hljs-string">vEX-01</span>
  <span class="hljs-attr">Model    :</span> <span class="hljs-string">EX9214</span>
  <span class="hljs-attr">Serial   :</span> <span class="hljs-string">VM68BB99A1F4</span>
  <span class="hljs-attr">Version  :</span> <span class="hljs-number">24.</span><span class="hljs-string">4R1.9</span>
  <span class="hljs-attr">Uptime   :</span> <span class="hljs-number">1</span> <span class="hljs-string">hour,</span> <span class="hljs-number">45</span> <span class="hljs-string">minutes,</span> <span class="hljs-number">17</span> <span class="hljs-string">seconds</span>
</code></pre>
<h3 id="heading-notes-source-within-devfacts">Notes (source within <code>dev.facts</code>)</h3>
<ul>
<li><p><code>hostname</code> → device host name</p>
</li>
<li><p><code>model</code> → platform family</p>
</li>
<li><p><code>serialnumber | serial</code> → chassis serial</p>
</li>
<li><p><code>version</code> → Junos version string</p>
</li>
<li><p><code>RE0.up_time | uptime</code> → human-readable uptime</p>
</li>
</ul>
<h3 id="heading-verification-checklist">Verification Checklist</h3>
<ul>
<li><p>Can SSH to the device with same creds.</p>
</li>
<li><p>NETCONF is enabled: <code>set system services netconf ssh</code> → <code>commit</code>.</p>
</li>
<li><p>No XML parsing here -<code>dev.facts</code> is already a Python dict.</p>
</li>
</ul>
<h3 id="heading-troubleshooting-quick">Troubleshooting (quick)</h3>
<ul>
<li><p>Connection/timeout → check reachability and NETCONF.</p>
</li>
<li><p>Auth errors → confirm user class/SSH access.</p>
</li>
<li><p>Missing uptime on some platforms → the fallback to <code>uptime</code> covers it.</p>
</li>
</ul>
<h2 id="heading-standard-section-part-3-read-devfacts-from-multiple-devices">STANDARD SECTION: PART 3 — Read <code>dev.facts</code> from multiple devices</h2>
<p>Task<br />• Gather the same five-field summary (Hostname, Model, Serial, Version, Uptime) from <strong>four devices</strong> using a simple Python list and a straight loop (no concurrency yet).</p>
<p>Inputs<br />• Devices reachable at 10.30.0.241–.244 with NETCONF over SSH enabled.<br />• One username/password with read access.</p>
<h3 id="heading-procedure-1">Procedure</h3>
<ol>
<li><p>Define a Python list of device IPs.</p>
</li>
<li><p>Loop over the list; for each IP, open a NETCONF session with <code>Device(...)</code>.</p>
</li>
<li><p>Read <code>dev.facts</code>, build the five-field summary with basic fallbacks, print.</p>
</li>
<li><p>Catch exceptions <strong>per device</strong> so one failure doesn’t stop the loop.</p>
</li>
</ol>
<h3 id="heading-code-lb001factsmultipy">CODE - lb001_facts_multi.py</h3>
<pre><code class="lang-python"><span class="hljs-comment"># lb001_facts_multi.py</span>
<span class="hljs-comment"># Purpose: read dev.facts from multiple devices (sequential loop) and print a short summary for each.</span>

<span class="hljs-keyword">from</span> jnpr.junos <span class="hljs-keyword">import</span> Device

HOSTS = [<span class="hljs-string">"10.30.0.241"</span>, <span class="hljs-string">"10.30.0.242"</span>, <span class="hljs-string">"10.30.0.243"</span>, <span class="hljs-string">"10.30.0.244"</span>]
USERNAME = <span class="hljs-string">"admin"</span>
PASSWORD = <span class="hljs-string">"yourpassword"</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">summarize</span>(<span class="hljs-params">facts: dict</span>) -&gt; dict:</span>
    <span class="hljs-string">"""Return the five-field summary with simple, portable fallbacks."""</span>
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"Hostname"</span>: facts.get(<span class="hljs-string">"hostname"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Model"</span>:    facts.get(<span class="hljs-string">"model"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Serial"</span>:   facts.get(<span class="hljs-string">"serialnumber"</span>) <span class="hljs-keyword">or</span> facts.get(<span class="hljs-string">"serial"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Version"</span>:  facts.get(<span class="hljs-string">"version"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Uptime"</span>:   facts.get(<span class="hljs-string">"RE0"</span>, {}).get(<span class="hljs-string">"up_time"</span>) <span class="hljs-keyword">or</span> facts.get(<span class="hljs-string">"uptime"</span>, <span class="hljs-string">"N/A"</span>),
    }

<span class="hljs-keyword">for</span> ip <span class="hljs-keyword">in</span> HOSTS:
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> Device(host=ip, user=USERNAME, passwd=PASSWORD) <span class="hljs-keyword">as</span> dev:
            f = dict(dev.facts)
            s = summarize(f)
            print(<span class="hljs-string">f"\nDevice: <span class="hljs-subst">{ip}</span>"</span>)
            <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> s.items():
                print(<span class="hljs-string">f"  <span class="hljs-subst">{k}</span>: <span class="hljs-subst">{v}</span>"</span>)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-comment"># Keep going even if one device fails</span>
        print(<span class="hljs-string">f"[error] <span class="hljs-subst">{ip}</span>: <span class="hljs-subst">{e}</span>"</span>)
</code></pre>
<h3 id="heading-expected-output-shape-1">Expected Output (shape)</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">Device:</span> <span class="hljs-number">10.30</span><span class="hljs-number">.0</span><span class="hljs-number">.241</span>
  <span class="hljs-attr">Hostname :</span> <span class="hljs-string">vEX-01</span>
  <span class="hljs-attr">Model    :</span> <span class="hljs-string">EX9214</span>
  <span class="hljs-attr">Serial   :</span> <span class="hljs-string">VM68BB99A1F4</span>
  <span class="hljs-attr">Version  :</span> <span class="hljs-number">24.</span><span class="hljs-string">4R1.9</span>
  <span class="hljs-attr">Uptime   :</span> <span class="hljs-number">1</span> <span class="hljs-string">hour,</span> <span class="hljs-number">45</span> <span class="hljs-string">minutes,</span> <span class="hljs-number">17</span> <span class="hljs-string">seconds</span>

<span class="hljs-attr">Device:</span> <span class="hljs-number">10.30</span><span class="hljs-number">.0</span><span class="hljs-number">.242</span>
  <span class="hljs-attr">Hostname :</span> <span class="hljs-string">vEX-02</span>
  <span class="hljs-attr">Model    :</span> <span class="hljs-string">EX9214</span>
  <span class="hljs-attr">Serial   :</span> <span class="hljs-string">VM...</span>
  <span class="hljs-attr">Version  :</span> <span class="hljs-number">24.</span><span class="hljs-string">4R1.9</span>
  <span class="hljs-attr">Uptime   :</span> <span class="hljs-number">1</span> <span class="hljs-string">hour,</span> <span class="hljs-number">42</span> <span class="hljs-string">minutes,</span> <span class="hljs-number">03</span> <span class="hljs-string">seconds</span>
<span class="hljs-string">...</span>
</code></pre>
<h3 id="heading-code-breakdownnotes-new-concepts-introduced">Code Breakdown/Notes (new concepts introduced)</h3>
<p>• Python list (<code>HOSTS</code>)<br />– An ordered collection. Iteration preserves order, so output blocks appear in the same order as the list.<br />– Easy to swap in different sets (lab vs. prod) without changing logic.</p>
<p>• <code>for</code> loop over <code>HOSTS</code><br />– Executes the same steps for each device: open session → read facts → print summary.<br />– Control flow is <strong>sequential</strong>; each iteration finishes before the next begins.</p>
<p>• Context manager inside the loop (<code>with Device(...) as dev</code>)<br />– Opens a NETCONF session for <strong>that</strong> device and guarantees it closes at the end of the iteration, even on errors.<br />– Prevents session leaks when iterating over many devices.</p>
<p>• Per-device <code>try/except</code><br />– Errors are isolated to the failing IP; the loop continues to the next device.<br />– This is critical in network automation—partial success is still useful.</p>
<p>• Small, pure helper (<code>summarize</code>)<br />– A “pure function”: input facts dict → output summary dict, no side effects.<br />– Makes the loop readable and testable; you can unit-test <code>summarize</code> with a mocked facts dict.</p>
<p>• Portability fallbacks in <code>summarize</code><br />– <code>serialnumber | serial</code>, <code>RE0.up_time | uptime</code> cover common platform differences without extra conditionals.</p>
<p>• Performance model (sequential, I/O-bound)<br />– Total runtime ≈ sum of individual device times (<code>T_total ≈ t1 + t2 + t3 + t4</code>).<br />– Good baseline for correctness; we’ll improve wall-clock time with concurrency next while keeping the same output format.</p>
<h2 id="heading-standard-section-part-4-concurrent-facts-threadpoolexecutor-ascompleted">STANDARD SECTION: PART 4 — Concurrent facts (ThreadPoolExecutor + as_completed)</h2>
<p><strong>Task</strong><br />• Gather the same five-field summary (Hostname, Model, Serial, Version, Uptime) from multiple devices <strong>concurrently</strong> using Python threads. Keep output format identical to Part 3 so results are comparable.</p>
<p><strong>Inputs</strong><br />• Devices reachable at 10.30.0.241–.244 with NETCONF over SSH enabled.<br />• One username/password with read access.</p>
<p><strong>Procedure</strong></p>
<ol>
<li><p>Reuse the exact <code>summarize_facts</code> helper from Part 3 (same five fields, same fallbacks).</p>
</li>
<li><p>Write a worker that connects to one device and returns three things:<br /> <code>(ip, summary_dict_or_None, error_message_or_None)</code>.</p>
</li>
<li><p>Create a <code>ThreadPoolExecutor</code> with a small, sensible <code>max_workers</code> (e.g., <code>min(8, len(HOSTS))</code>).</p>
</li>
<li><p>Submit one worker per IP; keep the returned <strong>Future</strong> objects in a list.</p>
</li>
<li><p>Iterate <code>as_completed(futures)</code> and print each device’s summary as soon as its Future finishes (fastest-first).</p>
</li>
<li><p>If a worker returns an error string, print a one-line <code>[error] &lt;ip&gt;: &lt;reason&gt;</code> and continue.</p>
</li>
</ol>
<h3 id="heading-code-lb001factsconcurrentpy">CODE - lb001_facts_concurrent.py</h3>
<pre><code class="lang-python"><span class="hljs-comment"># lb001_facts_concurrent.py</span>
<span class="hljs-comment"># Purpose: read dev.facts from multiple devices concurrently (threads) and print a short summary for each.</span>

<span class="hljs-keyword">from</span> concurrent.futures <span class="hljs-keyword">import</span> ThreadPoolExecutor, as_completed
<span class="hljs-keyword">from</span> jnpr.junos <span class="hljs-keyword">import</span> Device

HOSTS = [<span class="hljs-string">"10.30.0.241"</span>, <span class="hljs-string">"10.30.0.242"</span>, <span class="hljs-string">"10.30.0.243"</span>, <span class="hljs-string">"10.30.0.244"</span>]
USERNAME = <span class="hljs-string">"admin"</span>
PASSWORD = <span class="hljs-string">"yourpassword"</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">summarize</span>(<span class="hljs-params">facts: dict</span>) -&gt; dict:</span>
    <span class="hljs-string">"""Return the five-field summary with simple, portable fallbacks."""</span>
    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"Hostname"</span>: facts.get(<span class="hljs-string">"hostname"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Model"</span>:    facts.get(<span class="hljs-string">"model"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Serial"</span>:   facts.get(<span class="hljs-string">"serialnumber"</span>) <span class="hljs-keyword">or</span> facts.get(<span class="hljs-string">"serial"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Version"</span>:  facts.get(<span class="hljs-string">"version"</span>, <span class="hljs-string">"N/A"</span>),
        <span class="hljs-string">"Uptime"</span>:   facts.get(<span class="hljs-string">"RE0"</span>, {}).get(<span class="hljs-string">"up_time"</span>) <span class="hljs-keyword">or</span> facts.get(<span class="hljs-string">"uptime"</span>, <span class="hljs-string">"N/A"</span>),
    }

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_summary</span>(<span class="hljs-params">ip: str</span>) -&gt; tuple[str, dict | <span class="hljs-keyword">None</span>, str | <span class="hljs-keyword">None</span>]:</span>
    <span class="hljs-string">"""
    Worker: connect to 'ip', read dev.facts, return (ip, summary, error).
    Catch exceptions here so the main thread stays clean.
    """</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">with</span> Device(host=ip, user=USERNAME, passwd=PASSWORD) <span class="hljs-keyword">as</span> dev:
            f = dict(dev.facts)
            <span class="hljs-keyword">return</span> ip, summarize(f), <span class="hljs-literal">None</span>
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> ip, <span class="hljs-literal">None</span>, str(e)

max_workers = min(<span class="hljs-number">8</span>, len(HOSTS))  <span class="hljs-comment"># sensible cap for small labs</span>

<span class="hljs-keyword">with</span> ThreadPoolExecutor(max_workers=max_workers) <span class="hljs-keyword">as</span> pool:
    futures = [pool.submit(fetch_summary, ip) <span class="hljs-keyword">for</span> ip <span class="hljs-keyword">in</span> HOSTS]

    <span class="hljs-comment"># Print each result as soon as it's ready (fastest-first)</span>
    <span class="hljs-keyword">for</span> fut <span class="hljs-keyword">in</span> as_completed(futures):
        ip, summary, err = fut.result()
        <span class="hljs-keyword">if</span> err:
            print(<span class="hljs-string">f"[error] <span class="hljs-subst">{ip}</span>: <span class="hljs-subst">{err}</span>"</span>)
            <span class="hljs-keyword">continue</span>

        print(<span class="hljs-string">f"\nDevice: <span class="hljs-subst">{ip}</span>"</span>)
        <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> summary.items():
            print(<span class="hljs-string">f"  <span class="hljs-subst">{k}</span>: <span class="hljs-subst">{v}</span>"</span>)
</code></pre>
<p>Expected Output (shape)<br />• Same per-device block as Part 3.<br />• Device blocks may appear <strong>out of list order</strong> (they print as soon as each device finishes). Example:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Device:</span> <span class="hljs-number">10.30</span><span class="hljs-number">.0</span><span class="hljs-number">.243</span>
  <span class="hljs-attr">Hostname :</span> <span class="hljs-string">vEX-03</span>
  <span class="hljs-attr">Model    :</span> <span class="hljs-string">EX9214</span>
  <span class="hljs-attr">Serial   :</span> <span class="hljs-string">VM...</span>
  <span class="hljs-attr">Version  :</span> <span class="hljs-number">24.</span><span class="hljs-string">4R1.9</span>
  <span class="hljs-attr">Uptime   :</span> <span class="hljs-number">1</span> <span class="hljs-string">hour,</span> <span class="hljs-number">52</span> <span class="hljs-string">minutes,</span> <span class="hljs-number">11</span> <span class="hljs-string">seconds</span>

<span class="hljs-attr">Device:</span> <span class="hljs-number">10.30</span><span class="hljs-number">.0</span><span class="hljs-number">.241</span>
  <span class="hljs-attr">Hostname :</span> <span class="hljs-string">vEX-01</span>
  <span class="hljs-attr">Model    :</span> <span class="hljs-string">EX9214</span>
  <span class="hljs-attr">Serial   :</span> <span class="hljs-string">VM...</span>
  <span class="hljs-attr">Version  :</span> <span class="hljs-number">24.</span><span class="hljs-string">4R1.9</span>
  <span class="hljs-attr">Uptime   :</span> <span class="hljs-number">1</span> <span class="hljs-string">hour,</span> <span class="hljs-number">55</span> <span class="hljs-string">minutes,</span> <span class="hljs-number">03</span> <span class="hljs-string">seconds</span>
<span class="hljs-string">...</span>
</code></pre>
<h3 id="heading-code-breakdown-amp-cs-notes-new-concepts-introduced">Code breakdown &amp; CS notes (new concepts introduced)</h3>
<p>• I/O-bound concurrency with threads<br />– NETCONF calls spend most of their time <strong>waiting</strong> on network I/O. Threads overlap that waiting, reducing wall-clock time.<br />– Python’s GIL is <strong>not</strong> a blocker here because we’re not CPU-bound.</p>
<p>• Thread pool (executor)<br />– <code>ThreadPoolExecutor</code> creates a small pool of worker threads and schedules tasks for you.<br />– Keep <code>max_workers</code> reasonable to avoid oversubscribing device control planes (lab default: up to 8).</p>
<p>• Futures and <code>as_completed</code><br />– Submitting a worker returns a <strong>Future</strong>, which represents a result that will arrive later.<br />– <code>as_completed(futures)</code> yields each Future <strong>when it finishes</strong> (fastest-first), so output starts immediately instead of waiting for the slowest device.</p>
<p>• Worker signature and isolation<br />– Worker returns a tuple <code>(ip, summary, error)</code> so the caller can handle success/failure uniformly.<br />– Exceptions are caught <strong>inside</strong> the worker; one failing device doesn’t stop the run.</p>
<p>• Resource management per device<br />– The worker uses <code>with Device(...):</code> so each NETCONF session opens and closes cleanly inside its own thread.</p>
<p>• Naming for readability<br />– Use descriptive names: <code>gather_facts_for_device</code>, <code>summarize_facts</code>, <code>completed_future</code>.</p>
<p>• Output schema remains identical to Part 3<br />– Same five fields, same formatting. This makes concurrency a drop-in speed improvement without changing downstream expectations.</p>
]]></content:encoded></item></channel></rss>