<?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[codednetwork]]></title><description><![CDATA[codednetwork]]></description><link>https://codednetwork.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Apr 2026 14:16:53 GMT</lastBuildDate><atom:link href="https://codednetwork.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Ansible for Network Automation ]]></title><description><![CDATA[Ansible, developed by Red Hat, is an open-source platform. Our focus is on Ansible Core, the open-source version. Red Hat also provides a commercial product, Ansible Tower, which enhances Ansible Core]]></description><link>https://codednetwork.com/ansible-for-network-automation</link><guid isPermaLink="true">https://codednetwork.com/ansible-for-network-automation</guid><category><![CDATA[YAML]]></category><category><![CDATA[ansible]]></category><category><![CDATA[ansible-playbook]]></category><category><![CDATA[ansible-module]]></category><category><![CDATA[NetworkAutomation]]></category><category><![CDATA[RHEL]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 07 Apr 2026 07:42:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/0ac1708a-2e61-4f15-ad25-ca091cfcbf96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ansible, developed by Red Hat, is an open-source platform. Our focus is on Ansible Core, the open-source version. Red Hat also provides a commercial product, Ansible Tower, which enhances Ansible Core with features such as Role-Based Access Control (RBAC), secure network credential storage, and a RESTful API, among others.</p>
<h1>Why Ansible for Network Automation ?</h1>
<p>Network engineers have long relied on manual CLI workflows: SSH into a device, run show commands, copy-paste output into a spreadsheet, make a change, repeat. This approach is slow, prone to errors, and unscalable when managing numerous routers across multiple vendors.</p>
<p>Ansible changes the game. It's agentless, uses SSH/NETCONF under the hood, and treats your network configuration as <strong>code</strong> — version-controlled, repeatable, and auditable. For a multivendor environment running Cisco IOS-XR, Juniper vMX, and Nokia SR OS, Ansible speaks all three dialects fluently.</p>
<h1>Ansible Architecture : The Core building blocks</h1>
<p>Here's a visual breakdown of Ansible's architecture:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/675faa40-401f-4dc6-a9eb-85e675b970df.png" alt="" style="display:block;margin:0 auto" />

<h2>1. Inventory - Your network Source of Truth</h2>
<p>The inventory file informs Ansible about the devices that exist and how to access them. You can use either IP addresses or fully qualified domain names. In the upcoming example we’ll introduce how to create groups. We will organize devices into vendor groups for targeted plays.</p>
<p><strong>File</strong>: <code>inventory.yml</code></p>
<pre><code class="language-yaml">[cisco_iosxr]
rtr-cisco-01 ansible_host=172.20.20.15

[juniper_vmx]
rtr-junos-01 ansible_host=172.20.20.16

[nokia_sros]
rtr-nokia-01 ansible_host=172.20.20.13
rtr-nokia-02 ansible_host=172.20.20.14

[core_routers:children]
cisco_iosxr
juniper_vmx
nokia_sros
</code></pre>
<h2>2. Playbooks - Your automation Scripts</h2>
<p>The playbook is a file that contains your automation instructions. In other words, playbooks contain the individual tasks and workflows that you want to use to automate your network. The playbook is written in YAML and consists of one or more plays. Each play, in turn, includes one or more tasks.</p>
<p>Let’s take a look at an example playbook to better understand its structure</p>
<pre><code class="language-yaml">---
- name: Run Pre-Change Checks
  hosts: core_routers
  gather_facts: false
  tasks:
    - name: Include vendor-specific pre-check tasks
      include_tasks: "tasks/pre_check_{{ ansible_network_os }}.yml"
</code></pre>
<p>Our example playbook provides an overview of the structure to be used. You should already understand the basics of YAML and indentation. Ultimately, you need a YAML list of plays and a YAML list of tasks under the tasks key. Any slight indentation errors will result in error messages from Ansible when running your playbook, so precision is crucial.</p>
<h2>3. Modules - The Actions of Ansible</h2>
<p>Ansible modules perform a specific operation. For network automation, you'll use vendor-specific collections:</p>
<table>
<thead>
<tr>
<th>Vendor</th>
<th>Collection</th>
<th>Key Modules</th>
</tr>
</thead>
<tbody><tr>
<td>Cisco IOS-XR</td>
<td>cisco.iosxr</td>
<td>iosxr_command,iosxr_config, iosxr_facts</td>
</tr>
<tr>
<td>Juniper Junos</td>
<td>junipernetworks.junos</td>
<td>junos_command, junos_config, junos_facts</td>
</tr>
<tr>
<td>Nokia SROS</td>
<td>nokia.sros</td>
<td>sros_command, sros_config</td>
</tr>
</tbody></table>
<h3>connection types</h3>
<pre><code class="language-yaml"># group_vars/cisco_iosxr.yml
ansible_network_os: cisco.iosxr.iosxr
ansible_connection: ansible.netcommon.network_cli

# group_vars/juniper_vmx.yml
ansible_network_os: junipernetworks.junos.junos
ansible_connection: ansible.netcommon.netconf

# group_vars/nokia_sros.yml
ansible_network_os: nokia.sros
ansible_connection: ansible.netcommon.network_cli
</code></pre>
<h2>Variables and Vault Credentials</h2>
<p>Hardcoding passwords in playbooks is a career-limiting move. Use <strong>group_vars</strong> for non-sensitive data and Ansible Vault for <strong>secrets</strong>.</p>
<p>Since we are testing in a lab environment we are not using Ansible Vault but it is highly recommended in a Production environment.</p>
<h3>group_vars/nokia_sros.yml — Non-sensitive defaults</h3>
<pre><code class="language-yaml">ansible_network_os: nokia.sros
ansible_connection: ansible.netcommon.network_cli
ansible_user: "admin"
ansible_password: "{{ vault_ansible_password }}""
</code></pre>
<h3>Creating and using Ansible Vault</h3>
<pre><code class="language-yaml"># Create an encrypted secrets file
ansible-vault create group_vars/all/vault.yml

# Edit it later
ansible-vault edit group_vars/all/vault.yml
</code></pre>
<p>Inside the vault file :</p>
<pre><code class="language-yaml"># group_vars/all/vault.yml (encrypted at rest)
vault_ansible_password: "cOdeDNetw\(rk2#%\)"
</code></pre>
<p>Run playbooks with Vault :</p>
<pre><code class="language-yaml"># Prompt for vault password
ansible-playbook precheck.yml --ask-vault-pass
</code></pre>
<h1>Hands On - Multivendor Pre/Post Change Checks</h1>
<p>The scenario involves a maintenance window for BGP configuration changes across core routers. You need to capture the device state before and after the changes, then compare them for any differences.</p>
<table>
<thead>
<tr>
<th>Action</th>
<th>Ansible Command</th>
</tr>
</thead>
<tbody><tr>
<td>Run pre-checks</td>
<td>ansible-playbook -i inventory.yml precheck.yml</td>
</tr>
<tr>
<td>make changes</td>
<td>Manual or separate change playbook</td>
</tr>
<tr>
<td>Run post-checks</td>
<td>ansible-playbook -i inventory.yml postcheck.yml</td>
</tr>
<tr>
<td>Compare &amp; Validate</td>
<td>ansible-playbook -i inventory.yml compare.yml</td>
</tr>
</tbody></table>
<h2>Project Structure</h2>
<pre><code class="language-shell">pre-post-checks/
├── inventory.yml
├── precheck.yml
├── postcheck.yml
├── junos_checks.yml          
├── sros_checks.yml                           
├── reports/
└── group_vars/
</code></pre>
<div>
<div>💡</div>
<div>All the tasks are flattened into one directory. A separate <em>task </em>folder can be created</div>
</div>

<h3>Prerequisites</h3>
<pre><code class="language-python">pip install ansible
pip install ansible-pylibssh (optional
</code></pre>
<h3>Pre-check Playbook</h3>
<p><strong>File</strong> : <code>precheck.yml</code></p>
<pre><code class="language-yaml">- name: "PRE-CHANGE CHECKS"
  hosts: core_routers
  gather_facts: false
  vars:
    check_phase: "pre"
    timestamp: "{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}"
    output_base: "{{ playbook_dir }}/reports"

  tasks:
    - name: Debug output path
      debug:
         msg: "Will create: reports/{{ inventory_hostname }}/{{ timestamp }}"
      delegate_to: localhost

    - name: Create output directory
      shell: mkdir -p "{{ output_base }}/{{ inventory_hostname }}/{{ timestamp }}"
      delegate_to: localhost

    - name: Set output_dir fact per host
      set_fact:
        output_dir: "{{ output_base }}/{{ inventory_hostname }}/{{ timestamp }}" 

    - name: Run Cisco IOS-XR checks
      include_tasks: cisco_checks.yml
      when: ansible_network_os == "cisco.iosxr.iosxr"

    - name: Run Juniper Junos checks
      include_tasks: junos_checks.yml
      when: ansible_network_os == "junipernetworks.junos.junos"

    - name: Run Nokia SR OS checks
      include_tasks: sros_checks.yml
      when: ansible_network_os == "sros"
</code></pre>
<p><strong>File</strong> : <code>cisco_checks.yml</code></p>
<pre><code class="language-yaml"> - name: "[Cisco] Gather BGP summary"
  cisco.iosxr.iosxr_command:
    commands:
      - show bgp ipv4 unicast summary
      - show bgp ipv6 unicast summary
  register: cisco_bgp_output

- name: "[Cisco] Gather interface status"
  cisco.iosxr.iosxr_command:
    commands:
      - show interfaces brief
      - show ipv4 interface brief
  register: cisco_intf_output

- name: "[Cisco] Gather routing table summary"
  cisco.iosxr.iosxr_command:
    commands:
      - show route summary
      - show route ipv6 summary
  register: cisco_route_output

- name: "[Cisco] Debug output_dir"
  debug:
    msg: "output_dir is: {{ output_dir }}"
  delegate_to: localhost

- name: "[Cisco] Save pre-check output to file"
  copy:
    content: |
      === CISCO IOS-XR PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ cisco_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ cisco_bgp_output.stdout[1] }}

      --- INTERFACE STATUS ---
      {{ cisco_intf_output.stdout[0] }}

      --- ROUTE SUMMARY ---
      {{ cisco_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost
</code></pre>
<p><strong>File</strong> : <code>junos_checks.yml</code></p>
<pre><code class="language-yaml">- name: "[Junos] Gather BGP summary (structured)"
  junipernetworks.junos.junos_command:
    commands:
      - show bgp summary
    display: xml   # NETCONF returns structured XML
  register: junos_bgp_output

- name: "[Junos] Gather interface status"
  junipernetworks.junos.junos_command:
    commands:
      - show interfaces terse
  register: junos_intf_output

- name: "[Junos] Gather routing table"
  junipernetworks.junos.junos_command:
    commands:
      - show route summary
  register: junos_route_output

- name: "[Junos] Extract BGP peer count using XPath"
  set_fact:
    bgp_peer_count: &gt;-
      {{ junos_bgp_output.output[0] | regex_findall('peer-count.*?&lt;/peer-count&gt;')
         | length }}

- name: "[Junos] Save pre-check output"
  copy:
    content: |
      === JUNIPER vMX PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY ---
      {{ junos_bgp_output.stdout[0] | default(junos_bgp_output.output[0]) }}

      --- INTERFACES ---
      {{ junos_intf_output.stdout[0] }}

      --- ROUTING TABLE SUMMARY ---
      {{ junos_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost
</code></pre>
<p><strong>File</strong> : <code>sros_checks.yml</code></p>
<pre><code class="language-yaml">- name: "[Nokia] Gather BGP summary"
  community.network.sros_command:
    commands:
      - show router bgp summary
      - show router bgp summary family ipv6
  register: sros_bgp_output

- name: "[Nokia] Gather interface status"
  community.network.sros_command:
    commands:
      - show router interface
      - show port
  register: sros_intf_output

- name: "[Nokia] Gather routing table"
  community.network.sros_command:
    commands:
      - show router route-table summary
  register: sros_route_output

- name: "[Nokia] Save pre-check output"
  copy:
    content: |
      === NOKIA SR OS PRE-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ sros_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ sros_bgp_output.stdout[1] }}

      --- INTERFACES ---
      {{ sros_intf_output.stdout[0] }}

      --- ROUTE TABLE SUMMARY ---
      {{ sros_route_output.stdout[0] }}
    dest: "{{ output_dir }}/pre_check.txt"
  delegate_to: localhost
</code></pre>
<p><strong>File</strong> : <code>group_vars/cisco_ixr.yml</code></p>
<pre><code class="language-yaml">ansible_network_os: cisco.iosxr.iosxr
ansible_connection: ansible.netcommon.network_cli
ansible_user: "&lt;username&gt;"
ansible_password: "&lt;password&gt;"
</code></pre>
<div>
<div>💡</div>
<div>The <code>nokia sros</code> and<code> juniper</code> are similar to the above</div>
</div>

<h3>Run the Playbook</h3>
<p>Output :</p>
<pre><code class="language-python">ansible-playbook -i inventory.yml precheck.yml

PLAY [PRE-CHANGE CHECKS] ***************************************************************

TASK [Debug output path] ***************************************************************
ok: [rtr-cisco-01 -&gt; localhost] =&gt; {
    "msg": "Will create: reports/rtr-cisco-01/20260407_103940"
}
ok: [rtr-junos-01 -&gt; localhost] =&gt; {
    "msg": "Will create: reports/rtr-junos-01/20260407_103940"
}
ok: [rtr-nokia-01 -&gt; localhost] =&gt; {
    "msg": "Will create: reports/rtr-nokia-01/20260407_103940"
}
ok: [rtr-nokia-02 -&gt; localhost] =&gt; {
    "msg": "Will create: reports/rtr-nokia-02/20260407_103940"
}

TASK [Create output directory] ***************************************************************
changed: [rtr-nokia-02 -&gt; localhost]
changed: [rtr-nokia-01 -&gt; localhost]
changed: [rtr-cisco-01 -&gt; localhost]
changed: [rtr-junos-01 -&gt; localhost]

TASK [Set output_dir fact per host] ***************************************************************
ok: [rtr-junos-01]
ok: [rtr-cisco-01]
ok: [rtr-nokia-01]
ok: [rtr-nokia-02]

TASK [Run Cisco IOS-XR checks] ***************************************************************
skipping: [rtr-junos-01]
skipping: [rtr-nokia-01]
skipping: [rtr-nokia-02]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/cisco_checks.yml for rtr-cisco-01

TASK [[Cisco] Gather BGP summary] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Gather interface status] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Gather routing table summary] ***************************************************************
ok: [rtr-cisco-01]

TASK [[Cisco] Debug output_dir] ***************************************************************
ok: [rtr-cisco-01 -&gt; localhost] =&gt; {
    "msg": "output_dir is: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_103940"
}

TASK [[Cisco] Save pre-check output to file] ***************************************************************
changed: [rtr-cisco-01 -&gt; localhost]

TASK [Run Juniper Junos checks] ***************************************************************
skipping: [rtr-cisco-01]
skipping: [rtr-nokia-01]
skipping: [rtr-nokia-02]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/junos_checks.yml for rtr-junos-01

TASK [[Junos] Gather BGP summary (structured)] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Gather interface status] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Gather routing table] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Extract BGP peer count using XPath] ***************************************************************
ok: [rtr-junos-01]

TASK [[Junos] Save pre-check output] ***************************************************************
changed: [rtr-junos-01 -&gt; localhost]

TASK [Run Nokia SR OS checks] ***************************************************************
skipping: [rtr-cisco-01]
skipping: [rtr-junos-01]
included: /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/sros_checks.yml for rtr-nokia-01, rtr-nokia-02

TASK [[Nokia] Gather BGP summary] ***************************************************************
ok: [rtr-nokia-01]
ok: [rtr-nokia-02]

TASK [[Nokia] Gather interface status] ***************************************************************
ok: [rtr-nokia-02]
ok: [rtr-nokia-01]

TASK [[Nokia] Gather routing table] ***************************************************************
ok: [rtr-nokia-02]
ok: [rtr-nokia-01]

TASK [[Nokia] Save pre-check output] ***************************************************************
changed: [rtr-nokia-01 -&gt; localhost]
changed: [rtr-nokia-02 -&gt; localhost]

PLAY RECAP ***************************************************************
rtr-cisco-01               : ok=9    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-junos-01               : ok=9    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-nokia-01               : ok=8    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
rtr-nokia-02               : ok=8    changed=2    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0
</code></pre>
<p>File : <code>reports/rtr-cisco-01/20260407_103940/precheck.txt</code></p>
<pre><code class="language-python">=== CISCO IOS-XR PRE-CHECK: rtr-cisco-01 ===
Timestamp: 20260407_103948

--- BGP SUMMARY (IPv4) ---
BGP router identifier 10.10.10.3, local AS number 65000
BGP generic scan interval 60 secs
Non-stop routing is enabled
BGP table state: Active
Table ID: 0xe0000000   RD version: 2
BGP table nexthop route policy:
BGP main routing table version 2
BGP NSR Initial initsync version 2 (Reached)
BGP NSR/ISSU Sync-Group versions 0/0
BGP scan interval 60 secs

BGP is operating in STANDALONE mode.


Process       RcvTblVer   bRIB/RIB   LabelVer  ImportVer  SendTblVer  StandbyVer
Speaker               2          2          2          2           2           0

Neighbor        Spk    AS MsgRcvd MsgSent   TblVer  InQ OutQ  Up/Down  St/PfxRcd
10.10.10.1        0 65000  239205  239192        2    0    0     2w3d          0

--- BGP SUMMARY (IPv6) ---
% None of the requested address families are configured for instance 'default'(36210)

--- INTERFACE STATUS ---
Intf       Intf        LineP              Encap  MTU        BW
               Name       State       State               Type (byte)    (Kbps)
--------------------------------------------------------------------------------
                Lo0          up          up           Loopback  1500          0
              Lo100          up          up           Loopback  1500          0
                Nu0          up          up               Null  1500          0
     Mg0/RP0/CPU0/0          up          up               ARPA  1514    1000000
          Gi0/0/0/0          up          up               ARPA  9212    1000000
          Gi0/0/0/1  admin-down  admin-down               ARPA  1514    1000000
          Gi0/0/0/2          up          up               ARPA  1514    1000000

--- ROUTE SUMMARY ---
Route Source                     Routes     Backup     Deleted     Memory(bytes)
local                            2          0          0           416
connected                        1          1          0           416
application fib_mgr              0          0          0           0
vxlan                            0          0          0           0
static                           0          0          0           0
dagr                             0          0          0           0
bgp 65000                        0          0          0           0
isis 0                           6          1          0           1456
Total                            9          2          0           2288
</code></pre>
<h3>Post-check Playbook</h3>
<p>For this playbook we are still using the same construct as the <strong>pre-check</strong> playbook the only changes done are below</p>
<pre><code class="language-yaml"># change the check_phase to post

- name: "POST-CHANGE CHECKS"
  hosts: core_routers
  gather_facts: false
  vars:
    check_phase: "post"

# change the sros_checks.yml like below 

- name: "[Nokia] Save post-check output"
  copy:
    content: |
      === NOKIA SR OS POST-CHECK: {{ inventory_hostname }} ===
      Timestamp: {{ timestamp }}

      --- BGP SUMMARY (IPv4) ---
      {{ sros_bgp_output.stdout[0] }}

      --- BGP SUMMARY (IPv6) ---
      {{ sros_bgp_output.stdout[1] }}

      --- INTERFACES ---
      {{ sros_intf_output.stdout[0] }}

      --- ROUTE TABLE SUMMARY ---
      {{ sros_route_output.stdout[0] }}
    dest: "{{ output_dir }}/post_check.txt"
  delegate_to: localhost

# Run the playbook 

ansible-playbook -i inventory.yml postcheck.yml
</code></pre>
<h3>Comparison Playbook</h3>
<p>The <code>compare.yml</code> playbook finds the most recent pre and post check files for every device — regardless of which timestamp folder they live in — diffs them, and saves a full validation report.</p>
<pre><code class="language-yaml">- name: "COMPARE PRE/POST CHECKS"
  hosts: localhost
  gather_facts: false

  tasks:
    - name: Find all device directories
      find:
        paths: "{{ playbook_dir }}/reports/"
        file_type: directory
        recurse: no
      register: device_dirs

    - name: Find latest pre-check for each device
      shell: |
        find "{{ item.path }}" -name "pre_check.txt" | sort | tail -1
      loop: "{{ device_dirs.files }}"
      register: latest_pre
      delegate_to: localhost

    - name: Find latest post-check for each device
      shell: |
        find "{{ item.path }}" -name "post_check.txt" | sort | tail -1
      loop: "{{ device_dirs.files }}"
      register: latest_post
      delegate_to: localhost

............. [truncated]
</code></pre>
<div>
<div>💡</div>
<div>Step 1 : <code>Find devices </code>-- Step 2: <code>Find latest pre/post</code> -- Step 3: <code>Diff pre/post</code> -- Step 4 : <code>Build Report</code> -- Step 5 : <code>Save and print</code></div>
</div>

<p>Sample diff output after the change window:</p>
<pre><code class="language-yaml">}
ok: [localhost] =&gt; (item=rtr-cisco-01) =&gt; {
    "msg": [
        "============================================================",
        "Device     : rtr-cisco-01",
        "Pre-check  : /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_103940/pre_check.txt",
        "Post-check : /labs/kleburu/python-projects/Ansible_Automation/multivendor-validation/pre-post-checks/reports/rtr-cisco-01/20260407_111244/post_check.txt",
        "============================================================",
        "--- DIFF ---",
        "1,2c1,2",
        "&lt; === CISCO IOS-XR PRE-CHECK: rtr-cisco-01 ===",
        "&lt; Timestamp: 20260407_103948",
        "---",
        "&gt; === CISCO IOS-XR POST-CHECK: rtr-cisco-01 ===",
        "&gt; Timestamp: 20260407_111252",
        "23c23",
        "&lt; 10.10.10.1        0 65000  239205  239192        2    0    0     2w3d          0",
        "---",
        "&gt; 10.10.10.1        0 65000  239272  239258        2    0    0     2w3d          0"
    ]
}
</code></pre>
<h1>Key Takeaways</h1>
<table>
<thead>
<tr>
<th>Benefit</th>
<th>Detail</th>
</tr>
</thead>
<tbody><tr>
<td>Speed</td>
<td>45 min of manual CLI work runs in under minutes across all vendors simultaneously.</td>
</tr>
<tr>
<td>Consistency</td>
<td>Every engineer captures the exact same data points — no more missed checks.</td>
</tr>
<tr>
<td>Auditability</td>
<td>All outputs are timestamped, stored, and diff-able. Perfect for change management.</td>
</tr>
<tr>
<td>Security</td>
<td>Ansible Vault keeps credentials out of your codebase, satisfying compliance requirements.</td>
</tr>
</tbody></table>
<h1>Download Code</h1>
<p>All YAML files are here: <a href="https://gitlab.com/kgosileburu/network-automation-scripts">codednetwork-week-8</a></p>
]]></content:encoded></item><item><title><![CDATA[NETCONF and YANG: Building Programmable Networks ]]></title><description><![CDATA[What is YANG?
YANG ( Yet Another Next Generation) is a data modeling language that defines the structure, semantics, and constraints of configuration and state data for network devices and services. Y]]></description><link>https://codednetwork.com/netconf-and-yang-building-programmable-networks</link><guid isPermaLink="true">https://codednetwork.com/netconf-and-yang-building-programmable-networks</guid><category><![CDATA[netconf]]></category><category><![CDATA[xml]]></category><category><![CDATA[yang]]></category><category><![CDATA[model driven app]]></category><category><![CDATA[schema]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 31 Mar 2026 07:24:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/2e3ced16-2bdb-4c5c-ac73-5ec250759a8d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What is YANG?</h1>
<p>YANG ( Yet Another Next Generation) is a data modeling language that defines the structure, semantics, and constraints of configuration and state data for network devices and services. YANG modules outline the hierarchical schema, including nodes, types, constraints, RPCs, and notifications. YANG has become the networking industry standard for data modeling because it is human readable, extensible and easy to learn</p>
<p>A YANG model defines a tree structure and data is mapped into this tree.  A model is defined in a text file and comprises a module and, optionally, submodules, which when compiled together form the tree.</p>
<p>YANG is a way to enforce constraints on data inputs. These may inputs used from an API encoded as XML and JSON. The device will check if the data adheres to the underlying model</p>
<h2>YANG Module Definition</h2>
<p><strong>module</strong> <code>nokia-types-bgp</code></p>
<pre><code class="language-json">module nokia-types-bgp {


yang-version "1.1";

namespace "urn:nokia.com:sros:ns:yang:sr:types-bgp";

prefix "types-bgp";

import nokia-sros-yang-extensions     { prefix "sros-ext"; }
import nokia-types-sros               { prefix "types-sros"; }

sros-ext:sros-major-release "rel22";

revision "2022-05-03";

typedef llgr-family-identifiers {
    type enumeration {
        enum "ipv4"                         { value 1; }
        enum "vpn-ipv4"                     { value 2; }
        enum "ipv6"                         { value 3; }
        enum "vpn-ipv6"                     { value 5; }
        enum "l2-vpn"                       { value 6; }
        enum "flow-ipv4"                    { value 10; }
        enum "route-target"                 { value 11; }
        enum "flow-ipv6"                    { value 14; }
        enum "label-ipv4"                   { value 17; }
        enum "label-ipv6"                   { value 18; }
        enum "flow-vpn-ipv4"                { value 23; }
        enum "flow-vpn-ipv6"                { value 24; }
    }

}
</code></pre>
<p>A <code>module name</code> is specified in the module <code>nokia-types-bgp</code> section, with <code>nokia-types-bgp</code> as the name of the new YANG module. A <code>yang-version</code> indicates the version number of the YANG definition used by the author, not the module itself. This is typically 1.0 or 1.1. For new modules, it's recommended to start with the latest version.</p>
<p>A <code>prefix</code> is a short name used within YANG modules for quick reference to the modules. An <code>import</code> is used alongside the module name of the YANG module being imported.</p>
<p>A <code>revision number</code> is formatted as a date and should be updated whenever the module changes. A <code>type definition (typedef)</code> defines custom types using standardized YANG elements.</p>
<h2>Exploring YANG in Depth</h2>
<h3>YANG STATEMENTS</h3>
<p>Leaf Nodes</p>
<ul>
<li><p>Simple data such as an integer or a string</p>
</li>
<li><p>Represents a single value</p>
</li>
<li><p>No children</p>
</li>
</ul>
<pre><code class="language-python"># YANG

leaf host-name { 
     type string;
     description "Hostname for this system"; 
}

# NETCONF XML 

&lt;host-name&gt;codednetwork.com&lt;/host-name&gt;
</code></pre>
<p>Leaf-List Nodes</p>
<ul>
<li><p>This is just like leaf statement but there can be multiple instances</p>
</li>
<li><p>one value of a particular type per leaf</p>
</li>
</ul>
<pre><code class="language-python"># YANG

leaf-list name-server { 
type string; 
description "List of DNS servers to query"
}

# NETCONF XML
 
&lt;name-server&gt;8.8.8.8&lt;/name-server&gt;
&lt;name-server&gt;4.4.4.4&lt;/name-server&gt;
</code></pre>
<p>List Nodes</p>
<ul>
<li><p>It allows you to create a list of leafs or leaf-lists</p>
</li>
<li><p>Each entry is structure or a record instance</p>
</li>
</ul>
<pre><code class="language-python"># YANG

list vlan { 
   key "id";
     leaf id { 
       type int; 
       range 1..4094; 
     } 
     leaf name { 
        type string;
     } 
}

# NETCONF XML 

&lt;vlan&gt; 
  &lt;id&gt;100&lt;/id&gt;
  &lt;name&gt;web_vlan&lt;/name&gt;
&lt;/vlan&gt;
&lt;vlan&gt; 
  &lt;id&gt;200&lt;/id&gt;
  &lt;name&gt;app_vlan&lt;/name&gt;
&lt;/vlan&gt;
</code></pre>
<p>Container Nodes</p>
<ul>
<li><p>It is used to group nodes in a subtree</p>
</li>
<li><p>It only contains child nodes and has no value</p>
</li>
<li><p>May contain number of child nodes of any type (including leafs, lists, containers and leaf-lists</p>
</li>
</ul>
<pre><code class="language-python"># YANG

container system {
    container login-control {
       container pre-login message {
              leaf message {
                   type (*); 
                   description
                   "Message displayed prior to the login prompt";
       }
    }
}

# NETCONF XML 

&lt;system&gt; 
      &lt;login-control&gt;
           &lt;pre-login message&gt;
                      &lt;message&gt; Learning Yang is cool &lt;/message&gt;
           &lt;/pre-login message&gt;
      &lt;/login-control&gt;
&lt;/system&gt;
</code></pre>
<h1>What is NETCONF ?</h1>
<h2>Overview</h2>
<p>NETCONF is a standardized IETF configuration management protocol specified in RFC 6241, known as <em>Network Configuration Protocol (NETCONF)</em> . It is secure, connection-oriented, and runs on top of the SSHv2 transport protocol as specified in RFC 6242 . NETCONF is an XML-based protocol that can be used as an alternative to CLI or SNMP for managing an SR OS router</p>
<p>NETCONF uses RPC messaging for communication between NETCONF client and NETCONF server running on SROS. An RPC message and configuration or state data is encapsulated within an XML document. The SR OS NETCONF interface supports configuration, state and various router operations</p>
<pre><code class="language-python">Client                                      Server  
|=== TRANSPORT ================================| 
|&lt;--- TCP SYN --------------------------------&gt;|
|&lt;-- TCP SYN-ACK -----------------------------&gt;|
|--- SSH handshake + userauth ----------------&gt;| 
|&lt;-- SSH userauth success ---------------------|
|--- SSH subsystem "netconf" request ---------&gt;| 
|&lt;-- SSH channel success ----------------------|
|                                              | 
|=== SESSION ==================================|
|&lt;--&lt;hello&gt; session-id=42, capabilities -------|
|---&lt;hello&gt; capabilities ---------------------&gt;| 
|                                              |
|=== OPERATIONS ===============================|
|--- &lt;rpc&gt; get-config(running) ---------------&gt;|
|&lt;--&lt;rpc-reply&gt; &lt;data&gt;…&lt;data&gt; -----------------|
|                                              |
|=== TEARDOWN =================================| 
|--- &lt;rpc&gt; close-session ---------------------&gt;| 
|&lt;--&lt;rpc-reply&gt; &lt;ok/&gt; -------------------------| 
|--- SSH channel close -----------------------&gt;|
</code></pre>
<p><em>message exchange</em></p>
<h2>Protocol Stack</h2>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/c3ad2ba7-9aad-4565-b0ba-ceec9b781169.png" alt="" style="display:block;margin:0 auto" />

<table>
<thead>
<tr>
<th><strong>Content</strong></th>
<th><strong>yang formatted information</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Operations</strong></td>
<td><strong>Specific functions that operators can do on the server (get-config, edit-config)</strong></td>
</tr>
<tr>
<td><strong>Messages</strong></td>
<td><strong>three main message types - <em>Remote Procedure Calls (RPCs)</em> : Instructions /requests formatted in XML . <em>Notification</em> : information provided from the server not in direct response to a RPC. <em>Hello</em> : Special message used in communication setup and capabilities discovery</strong></td>
</tr>
<tr>
<td><strong>Secure Transport</strong></td>
<td><strong>NETCONF is not a transport layer. It is layered on top of a secured-orientated transport protocol eg SSH , HTTP/TLS</strong></td>
</tr>
</tbody></table>
<h2>Enabling NETCONF Nokia SROS</h2>
<pre><code class="language-shell"># Step 1 - Ensure model-driven mode is enabled 

A:R1# configure system management-interface configuration-mode model-driven

# Step 2 - Enable NETCONF and Auto-save 

(gl)[configure system management-interface]
A:admin@R1# 
    netconf { 
    admin-state enable
    auto-config-save true 
    }

# Step 3 - Select the YANG Modules 

(gl)[configure system management-interface]
A:admin@R1#
    yang-modules { 
       nokia-modules false 
       nokia-combined-modules true
    }

# Step 4 - Create user with NETCONF access 

(gl)[configure local-user { 
        user "netconf" { 
            password "&lt;password-here&gt;" 
            access { 
                netconf true 
            } 
            console { 
               member ["administrative"] 
            } 
        } 
} system security user-params]

# Step 5 - Grant lock and kill permissions 

(gl)[configure system security aaa local-profiles profile "administrative"] A:admin@R1#
    netconf { 
        base-op-authorization {
            kill-session true 
            lock true
        }
    }
</code></pre>
<h2>NETCONF - Base Operations</h2>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/7c40db98-434b-481d-a70d-1370cf411d61.png" alt="" style="display:block;margin:0 auto" />

<h2>NETCONF - Interacting with Nokia SROS</h2>
<h3>Connect with SSH</h3>
<p>The simplest and the easiest way to interact with a router is by using SSH.</p>
<pre><code class="language-python">ssh admin@&lt;host-ip&gt; -p 830 -s netconf
</code></pre>
<div>
<div>💡</div>
<div><strong><em>&lt;hello&gt; </em></strong><em>returned with router capabilities</em></div>
</div>

<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"&gt;
    &lt;capabilities&gt;
       &lt;capability&gt;urn:ietf:params:netconf:base:1.0&lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:base:1.1&lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:candidate:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:confirmed-commit:1.1
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:rollback-on-error:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:notification:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:interleave:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:validate:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:validate:1.1
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:startup:1.0
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:url:1.0
       scheme=ftp,tftp,file&lt;&lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring
       &lt;/capability&gt;
       &lt;capability&gt;urn:nokia.com:sros:ns:yang:sr:major-release:24
       &lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:xml:ns:yang:iana-if-type?module=iana-if-   
       type&amp;revision=2014-05-08&lt;/capability&gt;
       &lt;capability&gt;urn:ietf:params:netconf:capability:yang-library:1.0?    
        revision=2016-06-21&amp;module-set-id=gOPkB60aiVyk5&lt;
       &lt;/capability&gt;
    &lt;/capabilities&gt;
     &lt;/session-id&gt;57677&lt;session-id&gt;
&lt;/hello&gt;
</code></pre>
<p><em>truncated capabilities</em></p>
<div>
<div>💡</div>
<div>Send a hello (mandatory first step) finish the message with ]]&gt;]]&gt;</div>
</div>

<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"&gt;
&lt;capabalities&gt;
&lt;capability&gt;urn:ietf:params:netconf:base:1.0&lt;/capability&gt;
&lt;/capabilities&gt;
&lt;/hello&gt;
]]&gt;]]&gt;
</code></pre>
<div>
<div>💡</div>
<div>&lt;get-config&gt; retrieving the services from the configuration datastore</div>
</div>

<pre><code class="language-xml"># Retrieve the services
 
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;rpc message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"&gt;
  &lt;get-config&gt;
    &lt;source&gt;&lt;candidate/&gt;&lt;/source&gt; 
    &lt;filter&gt;
      &lt;configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"&gt;
        &lt;service&gt;
        &lt;/service&gt;
      &lt;/configure&gt;
    &lt;/filter&gt;
  &lt;/get-config&gt;
&lt;/rpc&gt;
]]&gt;]]&gt;

# RPC-REPLY with services 

&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;rpc-reply message-id="101" xmlns="urn:ietf:params:xml:ns:netconf:base:1.0&gt;
   &lt;data&gt;
       &lt;configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf" xmlns:nokia-attr="urn:nokia.com:sros:ns:yang:sr:attributes"&gt;
           &lt;service&gt;
               &lt;epipe&gt;
                   &lt;service-name&gt;test&lt;/service-name&gt;
                   &lt;admin-state&gt;enable&lt;/admin-state&gt;
                   &lt;service-id&gt;10&lt;/service-id&gt;
                   &lt;customer&gt;1&lt;/customer&gt;
                   &lt;spoke-sdp&gt;
                      &lt;sdp-bind-id&gt;10:10&lt;/sdp-bind-id&gt;
                      &lt;admin-state&gt;enable&lt;/admin-state&gt;
                   &lt;/spoke-sdp&gt;
                &lt;/epipe&gt;
                &lt;sdp&gt;
                  &lt;sdp-id&gt;10&lt;/sdp-id&gt;
                  &lt;admin-state&gt;enable&lt;/admin-state&gt;
                  &lt;description&gt;pysros-example&lt;/description&gt;
                  &lt;delivery-type&gt;mpls&lt;/delivery-type&gt;
                  &lt;far-end&gt; 
                      &lt;ip-address&gt;10.10.10.2&lt;/ip-address&gt;
                  &lt;/far-end&gt;
                  &lt;lsp&gt;
                    &lt;lsp-name&gt;PE1-to-PE2&lt;/lsp-name&gt;
                  &lt;/lsp&gt;
                &lt;/sdp&gt;
         &lt;/service&gt;
       &lt;/configure&gt;
   &lt;/data&gt;
&lt;/rpc-reply&gt;
</code></pre>
<h3>Connect with netconf-console2</h3>
<p><code>netconf-console2</code> is a console application built to interact with network devices using NETCONF. It allows engineers and automation systems to send NETCONF Remote Procedure Call (RPC) operations to a device. It can run in two modes:</p>
<ul>
<li><p><strong>Command-line mode:</strong> Used to execute one or more RPC operations in a single shell command.</p>
</li>
<li><p><strong>Interactive (console) mode:</strong> Provides an interactive session where a user can issue multiple commands sequentially, with limited support for tab-completion</p>
</li>
</ul>
<div>
<div>💡</div>
<div>In this below example we just show retrieving configuration using &lt;get-config&gt;</div>
</div>

<pre><code class="language-python"># Install netconf-console2 
pip install netconf-console2 

# Retrieve configuration 

netconf-console2 --host=&lt;host-ip&gt; -u &lt;username&gt; -p &lt;password&gt; --port=830 --get-config

# Configuration results 

&lt;?xml version='1.0' encoding='UTF-8'?&gt; 
&lt;data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"&gt;
        &lt;configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf" xmlns:nokia-attr="urn:nokia.com:sros:ns:yang:sr:attributes"&gt;
            &lt;groups&gt;
                &lt;group&gt; 
                    &lt;name&gt;test&lt;/name&gt;
                    &lt;router&gt;
                       &lt;router-name&gt; Base &lt;/router-name&gt;
                       &lt;interface&gt;loop&lt;/interface&gt;
                       &lt;ip-mtu&gt;444&lt;/ip-mtu&gt;
                       &lt;/interface&gt;
                    &lt;/router&gt;
                &lt;group&gt;              
            &lt;/groups&gt;
            &lt;card&gt;
                &lt;slot-number&gt;1&lt;/slot-number&gt;
                &lt;card-type&gt;iom-1&lt;card-type&gt;
                &lt;mda&gt;
                   &lt;mda-slot&gt;1&lt;/mda-slot&gt;
                   &lt;mda-type&gt;me12-100gb-qsfp28&lt;/mda-type&gt;
                &lt;/mda&gt;
            &lt;/card&gt;

............. [output truncated]
</code></pre>
<h3>Connect with NETCONF client for Visual Studio Code</h3>
<p>This extension adds an interactive NETCONF client to Visual Studio Code, connecting to NETCONF servers such as NOKIA IP routers (SR OS and SRLinux). It brings NETCONF into the editor so you can manage modern network equipment directly from VS Code using the standard NETCONF protocol.</p>
<p>If you're not familiar with Python or using libraries like NAPALM and ncclient to interact with a router running NETCONF, this VS Code extension is perfect for you. The interface is user-friendly and easy to navigate.</p>
<div>
<div>💡</div>
<div>Download the extension on VS Code Marketplace. Add the router with the correct NETCONF user and password before interaction</div>
</div>

<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/7bf98c06-2182-4d8c-99f9-778a5e7af3bd.jpg" alt="" style="display:block;margin:0 auto" />

<p><em>Create the connect-name , host-ip and enter the credentials</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/9c49d629-4121-4398-8837-10e00a6469fc.jpg" alt="" style="display:block;margin:0 auto" />

<p><em>Click connect icon to start interacting with the Nokia SROS</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/1e94783d-280f-479b-bef2-2287a9c9edb7.jpg" alt="" style="display:block;margin:0 auto" />

<p><em>Base operations options</em></p>
<h1>Other Tools</h1>
<h2>PYANG</h2>
<p>pyang is a YANG validator and converter that checks YANG modules for correctness and generates documentation or code stubs.</p>
<p><code>Convert YANG into a tree diagram</code></p>
<pre><code class="language-python"># Execute the below command 

pyang -f tree nokia-state.yang &gt; nokia-state.tree
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/7e8f7529-54f9-47c0-b812-761fd5ec0e04.png" alt="" style="display:block;margin:0 auto" />

<p><em>nokia-state tree structure sample</em></p>
<p><code>Generate html/javascript tree</code></p>
<pre><code class="language-python"># Execute the below command 

pyang -f jstree nokia-state.yang &gt; nokia-state.jstree
</code></pre>
<p>Then you can browse the YANG on a web browser</p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/fee9ed1a-3f8a-4aaf-b58d-c236de07f50d.jpg" alt="" style="display:block;margin:0 auto" />

<h1>Conclusion</h1>
<p>The CLI era gave network engineers power and flexibility, but at the cost of machine-readability, validation, and safe rollback. Every vendor implemented their own syntax, every script was fragile, and every change carried the risk of an unrecoverable typo. NETCONF and YANG fundamentally transform the way network configurations are managed , the device exposes a schema, accepts structured configuration, validates every change before it is deployed, and can roll back automatically.</p>
<p>That shift — from text scraping to model-driven management — is not just a technical improvement. It is what makes large-scale network automation reliable enough to trust in production. When your automation toolchain uses YANG, it knows what a valid interface configuration looks like before it ever touches a router. When it uses <em>confirmed commit</em>, a misconfiguration cannot disable a device permanently. When it subscribes to NETCONF notifications, it reacts to events rather than polling every 60 seconds hoping nothing changed.</p>
<blockquote>
<p><em>" The combination of a strongly-typed schema language, a transactional RPC protocol, and a mandatory encrypted transport is not accidental — it is a deliberate architecture designed for the scale and reliability demands of modern network operations "</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Automate Nokia SR OS with pySROS: A Practical Deep Dive
]]></title><description><![CDATA[What is pySROS and why does it matter?
Managing Nokia SR OS routers, such as the 7750 SR, 7450 ESS, 7210 SAS and the rest of the family, has typically involved dealing with CLI automation through Netm]]></description><link>https://codednetwork.com/automate-nokia-sr-os-with-pysros-a-practical-deep-dive</link><guid isPermaLink="true">https://codednetwork.com/automate-nokia-sr-os-with-pysros-a-practical-deep-dive</guid><category><![CDATA[Python]]></category><category><![CDATA[nokia]]></category><category><![CDATA[yang]]></category><category><![CDATA[model driven app]]></category><category><![CDATA[command line]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 24 Mar 2026 07:15:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/01a22a62-e276-4463-82cd-dc4bdbb02e48.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What is pySROS and why does it matter?</h1>
<p>Managing Nokia SR OS routers, such as the 7750 SR, 7450 ESS, 7210 SAS and the rest of the family, has typically involved dealing with CLI automation through Netmiko or creating a custom NETCONF client. While both methods are functional, they have drawbacks: screen-scraping CLI output can fail with software updates, and manually crafting NETCONF XML payloads is cumbersome and prone to errors.</p>
<p><strong>pySROS</strong> is Nokia's answer: pySROS libraries provide a model-driven management interface for Python developers to integrate with supported Nokia routers running the Service Router Operating System (SR OS). The libraries provide an <strong>Application Programming Interface</strong> (API) for developers to create applications that can interact with Nokia SR OS devices, whether those applications are executed from a development machine, a remote server, or directly on the router</p>
<div>
<div>💡</div>
<div><strong>Key insight</strong>: pySROS is YANG schema aware. Each element has knowledge of its path, model, and data type in the YANG model. The advertised capability provides information about the schemas supported by SR OS which allows a NETCONF client to query and retrieve schema information from the SR OS NETCONF server.</div>
</div>

<table>
<thead>
<tr>
<th>FEATURE</th>
<th>DESCRIPTION</th>
</tr>
</thead>
<tbody><tr>
<td>Model-driven</td>
<td>Full YANG schema awareness — Nokia native and OpenConfig modules</td>
</tr>
<tr>
<td>Write once, run anywhere</td>
<td>Same script on your laptop or directly on the SR OS node</td>
</tr>
<tr>
<td>NETCONF transport</td>
<td>Remote execution uses NETCONF/SSH; full config and state access</td>
</tr>
<tr>
<td>On-box MicroPython</td>
<td>SR OS ships a MicroPython interpreter for on-device automation</td>
</tr>
<tr>
<td>OpenConfig support</td>
<td>Works with OpenConfig YANG for multi-vendor normalisation</td>
</tr>
</tbody></table>
<h1>YANG Modeling</h1>
<p>YANG is a language that is designed to be readable by both humans and machines in order to model configuration and state information. YANG is rapidly becoming the standard way to model network devices and network device information. YANG is defined in the following RFCs:</p>
<ul>
<li><p><em>RFC 6020 - YANG - A Data Modelling Language for the Network Configuration Protocol (NETCONF)</em></p>
</li>
<li><p><em>RFC 6021 - Common YANG Data Types</em></p>
</li>
<li><p><em>RFC 7950 - The YANG 1.1 Data Modelling Language</em></p>
</li>
</ul>
<h2>Model paths</h2>
<p>At the core of the pySROS libraries are Nokia's model-driven management concepts built into SR OS. Communication between applications developed with pySROS libraries and Nokia SR OS routers is facilitated through model-driven paths that reference elements within the Service Router Operating System. The pySROS libraries use modeled paths in the JSON instance path format, which describes the referenced YANG models, including all YANG lists, list keys, and their values (though these may sometimes be omitted).</p>
<p>To obtain the JSON instance path directly from an SR OS router running software from release 21.7.R1, enter <code>pwc json-instance-path</code> in the MD-CLI within the relevant context.</p>
<p><em>SR OS</em> <code>pwc json-instance-path</code> <em>output from service configuration</em></p>
<pre><code class="language-plaintext">(gl)[/configure service vprn "10" bgp-ipvpn mpls] 
A:admin@R1# pwc json-instance-path 
Present Working Context: /nokia-conf:configure/service/vprn[service-name="10"]/bgp-ipvpn/mpls
</code></pre>
<p><em>SR OS</em> <code>pwc json-instance-path</code> <em>output from BGP State</em></p>
<pre><code class="language-plaintext">(gl)[/state router "Base" bgp neighbor "10.10.10.4"] 
A:admin@R1# pwc json-instance-path
Present Working Context: /nokia-state:state/router[router-name="Base"]/bgp/neighbor[ip-address="10.10.10.4"]
</code></pre>
<div>
<div>💡</div>
<div>Data is obtained or configured via the pySROS libraries using these path formats</div>
</div>

<h1>Prerequisites and Installation</h1>
<p>To use the pySROS libraries, the following pre-requisites must be met:</p>
<ul>
<li><p>one or more SR OS nodes running in model-driven management interface configuration mode</p>
</li>
<li><p>Running SR OS 21.7.R1 or greater (to execute applications on the SR OS device)</p>
</li>
<li><p>With NETCONF enabled and accessible by an authorized user (to execute applications remotely)</p>
</li>
<li><p>Python 3 interpreter of version 3.6 or newer when using the pySROS libraries to execute applications remotely</p>
</li>
</ul>
<pre><code class="language-python">#Create and activate a virtual environment
python3 -m venv env
source env/bin/activate

#Install pySROS from PyPI
pip install pysros
pip install --upgrade pysros

#Or clone from GitHub
git clone https://github.com/nokia/pysros 
python3 setup.py install
</code></pre>
<h1>Architecture</h1>
<h2>On-box vs Off-box execution</h2>
<p>pySROS supports two distinct execution modes. Understanding the trade-offs is the first design decision for any automation project.</p>
<pre><code class="language-python">Off-box: [Python script] ──── NETCONF/SSH ────▶ [SR OS node]
        dev machine / automation server          7750 SR / 7250 IXR

On-box: [SR OS node] ▶ [pyexec /cron /EHS/alias] ▶ [pySROS script]
                                               MicroPython Interpreter
</code></pre>
<p><strong>Off-box</strong> execution gives you several advantages to name a few: the ability to interact with multiple routers from a single script, executing scripts with a large data set, no impact on CPU / memory, Easier integration with other systems and the most important one is Access to richer Python ecosystem (Prometheus, Grafana, NetBox, Streamlit, etc.)</p>
<p><strong>On-box</strong> execution runs inside the router itself via MicroPython — a lean Python 3 implementation designed for constrained environments. Execution time and memory usage are bounded by SR OS to protect routing stability. You can only address the local device and and the script can be triggered via CLI command aliases, EHS event handlers, cron jobs, or the <code>pyexec</code> command.</p>
<div>
<div>💡</div>
<div>Python applications are configured in the <code>configure python</code> context. The<code> pyexec</code> command takes a parameter, the <em>name</em> of a Python application from the SR OS configuration <code>configure&gt;python&gt;python-script&gt;application_name</code> or the URL to the location (local/remote) of a Python application</div>
</div>

<h1>Getting Started</h1>
<h2>Lab Setup</h2>
<p>We continue using the lab setup we built for our multivendor environment, and this is made possible by <a href="https://containerlab.dev/">Containerlab</a></p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/03dc5158-a6ad-4972-95e7-771310793a3e.png" alt="" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>TopoViewer - Interactive topology visualization and editing for <em>Containerlab </em>network labs directly in VS Code.</div>
</div>

<p>Our lab consists of the below components:</p>
<p>Servers:</p>
<ul>
<li><p>Rocky Linux 9.6</p>
</li>
<li><p>Python 3.9</p>
</li>
<li><p>pySROS 23.3.3</p>
</li>
</ul>
<p>Network Devices:</p>
<ul>
<li>Nokia SR OS 24.10.R5</li>
</ul>
<h2>Making a connection</h2>
<p>To access the model-driven interface of SR OS while running a Python application from a remote workstation, use the <code>pysros.management.connect()</code> method. When executing a Python application on SR OS, the same method is used, but its arguments are ignored</p>
<p><strong>File</strong> : <code>connect.py</code></p>
<pre><code class="language-python">from pysros.management import connect
from pysros.exceptions import *
import sys


def get_connection(): 
    try: connection_object = connect(host="&lt;host-ip&gt;", 
                                     username="username", 
                                     password="password")

    except RuntimeError as error1:
        print("Failed to connect.  Error:", error1)
        sys.exit(-1)
    except ModelProcessingError as error2:
        print("Failed to create model-driven schema.  Error:", error2)
        sys.exit(-2)

if __name__ == "__main__":
    connection_object = get_connection()
    print("\n Connection established successfully \n")
</code></pre>
<p><strong>Expected Output</strong></p>
<pre><code class="language-python">Connection established successfully
</code></pre>
<h2>Compliance Tool</h2>
<p>The compliance script (<code>compliance.py</code>) connects to an SR OS router to validate its configuration against a predefined golden template, which consists of YANG model paths and their expected values.</p>
<p>For each check, it calls <code>c.running.get(path)</code> and compares the returned <code>.data</code> value against the expected string. If the path doesn't exist or the value doesn't match, it's recorded as a violation. At the end it prints a report showing passed vs failed checks, with the full YANG path, expected value, and actual value for each violation.</p>
<p><strong>File</strong> : <code>compliance.py</code></p>
<pre><code class="language-python">from pysros.management import connect
import sys
import datetime

GOLDEN_CHECKS = [
    # Security baseline
    (
        "/nokia-conf:configure/system/login-control/pre-login   message/message",
        "Authorized access only. All activity is monitored."
    ),
    (
        "/nokia-conf:configure/system/security/telnet-server",
        "False"
    ),
    (
        "/nokia-conf:configure/system/security/ftp-server",
        "True"
    ),

    # NTP must be enabled — correct path is under system/time/ntp
    (
        "/nokia-conf:configure/system/time/ntp/admin-state",
        "enable"
    ),
    # Router-id must exist (we just check presence, not value)
    (
        "/nokia-conf:configure/router[router-name='Base']/router-id",
        None
    ),
    # SNMP community must exist
    (
        "/nokia-conf:configure/system/security/snmp",
        None
    ),
    #  ISIS should be enabled
    (
        "/nokia-conf:configure/router[router-name='Base']/isis[isis-instance='0']/admin-state",
        "enable"
    ),
]

def check_compliance(c, checks):

    violations = []
    passed = 0

    for path, expected in checks:
        try:
            val = c.running.get(path)
            actual = val.data if hasattr(val, "data") else str(val)
            
            if expected is not None and str(actual) != str(expected):
                violations.append((path, expected, actual))
            else:
                passed += 1
       
        except Exception:
            violations.append((path, expected, "MISSING"))
    
        return violations, passed

def print_report(violations, passed, total):
    
    ts = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
    print()
    print("=" * 70)
    print(f"  Nokia SR OS Config Compliance Report")
    print(f"  Generated: {ts}")
    print("=" * 70)
    print(f"  Checks:  {total}  |  Passed: {passed}  |  Violations: {len(violations)}")
    print("=" * 70)
    
    if not violations:
        print()
        print("  [COMPLIANT]  All checks passed.")
        print()
        return
     
    print()
    print("  VIOLATIONS FOUND:")
    print()
    for i, (path, expected, actual) in enumerate(violations, 1):
        # Shorten very long paths for readability
        short_path = path.split("/")[-1] if "/" in path else path
        print(f"  [{i}] {short_path}")
        print(f"      Full path : {path}")
        print(f"      Expected  : {expected if expected is not None else '(must exist)'}")
        print(f"      Actual    : {actual}")
        print()

if __name__ == "__main__":
    c = connect(host="&lt;host-name&gt;" , username="username" , password="password")

    total = len(GOLDEN_CHECKS)
    violations, passed = check_compliance(c, GOLDEN_CHECKS)

    print_report(violations, passed, total)

    c.disconnect()
</code></pre>
<p><strong>Expected Output</strong></p>
<pre><code class="language-python">=============================================================
 Nokia SR OS Config Compliance 
 Report Generated: 2026-03-23 02:22:26 UTC
=============================================================
 Checks: 7 | Passed: 6 | Violations: 1
=============================================================

 VIOLATIONS FOUND:
 [1] admin-state
     Full path : /nokia-conf:configure/system/time/ntp/admin-state
     Expected  : enable
     Actual    : disable
</code></pre>
<div>
<div>💡</div>
<div>In this scenario ntp was <code>disabled </code>on the router and a <code>violation </code>was found</div>
</div>

<pre><code class="language-python"># Enabling NTP 

[gl:/configure system time ntp]
A:admin@PE1# info json
{ 
 "nokia-conf:admin-state": "enable"
}

# NTP was enabled and all checks passed with no violations 

============================================================
   Nokia SR OS Config Compliance 
   Report Generated: 2026-03-23 09:08:47 UTC
=============================================================
   Checks: 7 | Passed: 7 | Violations: 0
=============================================================
</code></pre>
<h2>CLI Command Alias</h2>
<p>A command alias in SR OS lets you expose a pySROS script as a native MD-CLI command. From the operator's perspective they just type a short command — the Python execution is invisible.</p>
<p><strong>How it works</strong></p>
<p>The script lives on the router's compact flash. SR OS's MD-CLI alias config points at it, and when the operator runs the alias, SR OS invokes the Python script to execute. It's the cleanest way to give network operators custom show commands without requiring them to know Python or pySROS exists.</p>
<p><strong>File</strong> : <code>interfaceproto.py</code></p>
<p><strong>Configuration</strong></p>
<pre><code class="language-python"># Step 1 - Configure the python-script 

[gl:/configure python python-script "interfaceproto"]
A:admin@PE1# info json
{
    "nokia-conf:admin-state": "enable",
    "nokia-conf:urls": ["cf3:\interfaceproto.py"],
    "nokia-conf:version": "python3"
}

# Step 2 - check the status of the Python Script 
/show python python-script "interfaceproto"
============================================================
Python script "interfaceproto"
============================================================
Description : (Not Specified)
Admin state : inService 
Oper state : inService
Oper state 
(distributed) : inService
Version : python3 
Action on fail: drop 
Protection : none
Primary URL : cf3:\interfaceproto.py
Secondary URL : (Not Specified) 
Tertiary URL : (Not Specified) 
Active URL : primary
Run as user : (Not Specified) 
Code size : 863 
Last changed : 03/23/2026 09:23:04

# Step 3 - Configure the command alias and mount it 

[gl:/configure system management-interface cli md-cli environment command-alias alias "intprotocol"]
info 
admin-state enable 
python-script "interfaceproto" 
mount-point "/show" { }

# Step 4 - Sanity check the alias configured 
A:admin@PE1# /show ?
Aliases: intprotocol - Command-alias

# Step 5 - Run the command protocols will be displayed for each interface 
A:admin@PE1# /show intprotocol
=============================================================
 Router Base Interfaces
=============================================================
 Interface          Oper State IPv4 State IPv4 Address 
 Protocols
-------------------------------------------------------------
 system             up          up        10.10.10.1
 isis mpls rsvp 
 tocisco_xrv9000    up          up        192.168.1.1 
 isis mpls rsvp ldp 
 toPE2              up          up        192.169.1.1 
 isis mpls rsvp ldp 
 loop               dormant     down      N/A
=============================================================
</code></pre>
<h1>Practical use cases</h1>
<ol>
<li><p><strong>Bulk interface auditing</strong> — Iterate over all interfaces across a fleet, collect IP/admin/oper-state into a structured report.</p>
</li>
<li><p><strong>EHS event-driven remediation</strong> — Trigger a pySROS script via EHS when a BGP session drops. Log context, attempt recovery, or notify an external system.</p>
</li>
<li><p><strong>Custom MD-CLI aliases</strong> — Wrap a pySROS script as an MD-CLI alias. Operators run a natural command without knowing Python is underneath.</p>
</li>
<li><p><strong>Scheduled config compliance</strong> — Run a pySROS script via SR OS cron hourly to validate config against a golden template and log deviations.</p>
</li>
<li><p><strong>Multi-vendor OpenConfig normalisation</strong> — Use OpenConfig YANG modules on SR OS alongside other vendors for a unified config and state model.</p>
</li>
</ol>
<h1>Conclusion</h1>
<p>pySROS represents a significant change in approaching Nokia SR OS automation. Traditionally, there has been operational friction between tasks performed from a management server and those executed on the router, involving different tools, credentials, failure modes, and duplicated logic. pySROS eliminates this divide by treating the execution context as a runtime detail rather than an architectural limitation.</p>
<table>
<thead>
<tr>
<th>Resource</th>
<th>URL</th>
</tr>
</thead>
<tbody><tr>
<td>pySROS official docs</td>
<td><a href="https://network.developer.nokia.com/static/sr/learn/pysros/latest/index.html">Learn PySROS</a></td>
</tr>
<tr>
<td>pySROS GitHub (examples)</td>
<td><a href="https://github.com/nokia/pysros">pySROS Git</a></td>
</tr>
<tr>
<td>SR OS YANG models</td>
<td><a href="http://github.com/nokia/7x50_YangModels">7X50 Yang Model</a></td>
</tr>
</tbody></table>
]]></content:encoded></item><item><title><![CDATA[Turn Scripts into Services: Why Network Automation needs a Web User Interface ?]]></title><description><![CDATA[Network automation has transformed how modern infrastructure teams operate. Scripts that once took hours of manual work — configuring devices, pulling inventory, validating compliance — now execute in]]></description><link>https://codednetwork.com/turn-scripts-into-services-why-network-automation-needs-a-web-user-interface</link><guid isPermaLink="true">https://codednetwork.com/turn-scripts-into-services-why-network-automation-needs-a-web-user-interface</guid><category><![CDATA[Python]]></category><category><![CDATA[NetworkAutomation]]></category><category><![CDATA[streamlit]]></category><category><![CDATA[Jinja2]]></category><category><![CDATA[YAML]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 17 Mar 2026 07:10:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/093f3a5c-81fa-4c9f-961b-35dff14b5988.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Network automation has transformed how modern infrastructure teams operate. Scripts that once took hours of manual work — configuring devices, pulling inventory, validating compliance — now execute in seconds. But there is a quite, persistent problem that undermines the full potential of these scripts: only the person who wrote them can use them.</p>
<p>This is the <strong>automation accessibility gap</strong>. A skilled network engineer writes a powerful Python script to query BGP neighbors, generate config diffs, or push bulk ACL changes. The script works beautifully — in their terminal, on their machine, with their environment variables set correctly. But the moment that engineer is unavailable, the capability disappears. Colleagues cannot run it. Managers cannot trigger it. The NOC team, the helpdesk, and even other engineers are locked out</p>
<blockquote>
<p><strong>"The best automation script is useless if only one person can run it."</strong></p>
</blockquote>
<p>The solution is not to simplify the scripts — it is to wrap them in an interface that makes them universally accessible. Web frameworks like <strong>Streamlit</strong> provide this capability, bridging the gap between powerful automation logic and the teams that need to use it every day.</p>
<h1>The Challenge of Terminal-Only Automation</h1>
<p>Before exploring the solution, it's important to understand why terminal-based automation struggles to scale across teams.</p>
<h2><strong>Expertise Dependency</strong></h2>
<p>Running a Python script involves understanding how to use a terminal, navigate directories, manage virtual environments, install dependencies, and set the correct flags and arguments. While network engineers find this routine, it poses a significant barrier for NOC analysts, security auditors, or project managers needing a one-time report. When automation is confined to the terminal, it centralizes power among a few and creates bottlenecks.</p>
<h2><strong>Fragile Environments</strong></h2>
<p>Scripts that run perfectly in one engineer's environment often fail in another's due to different Python versions, missing libraries, conflicting dependencies, or mismatched environment variables. Reproducing the right execution environment is a technical skill unto itself, and it consumes time that teams do not have during incidents or operational windows.</p>
<h2><strong>Auditability Challenges</strong></h2>
<p>When multiple engineers share scripts over email or shared drives, there is no audit trail. Who ran the script? Against which devices? With what parameters? What was the output? This opacity creates compliance risks, troubleshooting challenges, and accountability gaps — especially in regulated environments</p>
<h2><strong>Lack of Validation</strong></h2>
<p>Raw scripts often accept command-line arguments with minimal validation. This can lead to errors, such as passing the wrong subnet, targeting the incorrect device group, or bypassing a confirmation prompt. Without a proper interface, there's no convenient way to implement input validation, confirmation dialogs, or permission controls.</p>
<h1>Introducing Streamlit: Rapid UI for Network Automation</h1>
<p><strong>Streamlit</strong> is an open-source Python framework that allows developers to create web applications directly from Python scripts — no HTML, no JavaScript, no CSS required. For network engineers who already write Python, this is transformative. The same code that powers a script can be surfaced as a clean, interactive web app in a matter of hours</p>
<h2><strong>Why Streamlit is Ideal for Network Teams ?</strong></h2>
<p><strong>Streamlit</strong> was designed with data scientists and engineers in mind, not professional web developers. Its philosophy is that the person who understands the logic should be able to build the interface. For network automation, this is a perfect fit:</p>
<p>•        Pure Python: No new languages to learn. If you can write a Netmiko or NAPALM script, you can build a Streamlit app.</p>
<p>•        Rapid development: A basic interface can be wrapped around an existing script in under an hour.</p>
<p>•        Built-in widgets: Text inputs, dropdowns, file uploaders, progress bars, and data tables are all available with single-line function calls.</p>
<p>•        Real-time output: Streamlit supports streaming output, making it ideal for long-running tasks like multi-device configuration pushes.</p>
<p>•        Easy deployment: Apps run on any server accessible by the team, including VMs, containers, or internal platforms.</p>
<h2>How Streamlit Works (Behind the Scenes)</h2>
<p>Streamlit follows a <strong>script rerun model</strong>.</p>
<p>Every time a user interacts with the UI:</p>
<ul>
<li><p>The Python script reruns from top to bottom</p>
</li>
<li><p>UI state is preserved using <code>session_state</code></p>
</li>
</ul>
<p>Architecture flow:</p>
<pre><code class="language-python">User Interaction (Button / Input)
            ↓
Streamlit Reruns Script
            ↓
Updates UI Components
            ↓
Displays New Results
</code></pre>
<h2>Core Streamlit Components Explained</h2>
<h3>Text Elements</h3>
<pre><code class="language-python">st.title("Network Config Generator")
st.caption("Generate configs using Jinja2")
</code></pre>
<p>Used for headings and descriptions.</p>
<h3>Input Widgets</h3>
<pre><code class="language-python">device = st.text_input("Enter Router IP")
</code></pre>
<p>Common widgets:</p>
<ul>
<li><p>text_input</p>
</li>
<li><p>selectbox</p>
</li>
<li><p>checkbox</p>
</li>
<li><p>sliders</p>
</li>
<li><p>file uploader</p>
</li>
</ul>
<h3>Layout System (Professional Dashboards)</h3>
<pre><code class="language-python">if st.button("Generate Configuration"):
    run_automation()
</code></pre>
<p>Buttons trigger Python logic directly.</p>
<h3>Session State: The Most Important Concept</h3>
<p>Because Streamlit reruns scripts, it uses:</p>
<pre><code class="language-python">st.session_state
</code></pre>
<p>This acts like memory for:</p>
<ul>
<li><p>Templates</p>
</li>
<li><p>User inputs</p>
</li>
<li><p>Generated outputs</p>
</li>
</ul>
<h1>Real-World Use Case for Network Engineers</h1>
<h2>Configuration Generator (Jinja2 + YAML)</h2>
<p>In recent articles, we've discussed generating dynamic configurations with Jinja2 in a multi-vendor environment and the significance of configuration validation. Throughout the series, we developed several scripts for practical use. We will reuse our automation scripts and integrate them into an interactive, user-friendly interface. With Streamlit, you can quickly prototype a functional GUI without needing frontend skills.</p>
<blockquote>
<p>Streamlit transforms a Python script into a self-service tool, empowering teams with independence while maintaining the engineer's control over the underlying logic.</p>
</blockquote>
<h3>Streamlit in a Network Configuration Generator</h3>
<p>A typical architecture:</p>
<pre><code class="language-python">User Input (Template + YAML)
            ↓
Streamlit GUI
            ↓
Jinja2 Rendering Engine
            ↓
Generated Configuration Output
            ↓
Download or Deploy to Devices
</code></pre>
<h3>Comparing Streamlit vs Traditional Web Frameworks</h3>
<table>
<thead>
<tr>
<th><strong>Feature</strong></th>
<th>Streamlit</th>
<th>Flask/Django</th>
</tr>
</thead>
<tbody><tr>
<td>Learning Curve</td>
<td>Very Low</td>
<td>Medium–High</td>
</tr>
<tr>
<td>Frontend Required</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Speed of Development</td>
<td>Very Fast</td>
<td>Slower</td>
</tr>
<tr>
<td>Best Use Case</td>
<td>Internal tools &amp; dashboards</td>
<td>Full-scale web apps</td>
</tr>
</tbody></table>
<h3>Prerequisites</h3>
<pre><code class="language-python">pip install streamlit 
</code></pre>
<h3>Run the application</h3>
<pre><code class="language-python">streamlit run app.py 
</code></pre>
<h3>Try the Correct URL</h3>
<pre><code class="language-python">http://localhost:8501 # On the SAME machine
http://&lt;server-ip&gt;:8501 # If running on a server/VM
</code></pre>
<h3>Generated Output</h3>
<p>Network Configuration Generator UI application</p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/8a793908-d613-42f2-9dad-d24f42d186a7.png" alt="" style="display:block;margin:0 auto" />

<p>Load Nokia SROS example and Validate :</p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/466a5d29-6402-42ca-b1a0-59c48b1f63c7.png" alt="" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>YAML validation successful</div>
</div>

<p>Nokia SROS example Generate Configuration</p>
<img src="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/c6a84abb-2f45-4677-a632-d083df55b948.png" alt="" style="display:block;margin:0 auto" />

<div>
<div>💡</div>
<div>Configuration generated and Download Config</div>
</div>

<h1>Limitations of Streamlit</h1>
<p>While Streamlit is excellent for internal tools and dashboards, it is not designed for large-scale enterprise web platforms requiring complex authentication, microservices, or highly customized frontends. However, for automation GUIs and engineering tools, it is one of the most efficient solutions available.</p>
<h1><strong>Conclusion: Automation is a Team Sport</strong></h1>
<p>Network automation has historically been the domain of individual engineers writing scripts for personal use. Streamlit change that equation entirely. It provide the bridge between powerful automation logic and the diverse teams that need to act on it — without requiring every user to become a Python developer or a terminal power user.</p>
<p>Network automation is only truly effective when it is accessible to everyone. A script limited to a single user is a capability restricted to that individual. In contrast, a web application that any team member can utilize becomes a resource for the entire organization.</p>
<blockquote>
<p><strong>Build the script. Then build the interface. Automation is only as powerful as its reach.</strong></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Configuration Validation and Testing – Safe Network Changes in a Multi-Vendor Environment]]></title><description><![CDATA[Generating configurations with Jinja is powerful. Deploying them safely is engineering.

We explored how to use Jinja2 templates to standardize configurations across vendors. But generating configurat]]></description><link>https://codednetwork.com/configuration-validation-and-testing-safe-network-changes-in-a-multi-vendor-environment</link><guid isPermaLink="true">https://codednetwork.com/configuration-validation-and-testing-safe-network-changes-in-a-multi-vendor-environment</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[Python]]></category><category><![CDATA[YAML]]></category><category><![CDATA[yang]]></category><category><![CDATA[NetworkAutomation]]></category><category><![CDATA[template]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 10 Mar 2026 07:19:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68933f4690103a1d4a8d7df7/030acdda-5319-4441-9001-1ed35d170e47.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Generating configurations with Jinja is powerful. Deploying them safely is engineering.</p>
</blockquote>
<p>We explored how to use <strong>Jinja2 templates</strong> to standardize configurations across vendors. But generating configuration is only half the job.</p>
<p>The real question is:</p>
<blockquote>
<p>How do you make sure your generated configuration won’t break production?</p>
</blockquote>
<p>Before you push any configuration to production, you need to know if it's correct. One typo can bring down a network. One misconfigured route can create a routing loop. One wrong ACL can block critical traffic.</p>
<p><strong>The golden rule of network automation</strong>: Never deploy untested configurations.</p>
<h1>Why Configuration Validation Matters ?</h1>
<p>Network outages can be extremely costly for enterprises. Most unplanned outages result from human errors during configuration changes rather than hardware failures. Implementing a disciplined pre- and post-change validation workflow significantly reduces this risk by automating the comparison of network states before and after each change window.</p>
<p>In a multi-vendor environment, syntax and behavior differ:</p>
<table>
<thead>
<tr>
<th>Vendor</th>
<th>Interface Syntax</th>
<th>Commit Model</th>
<th>Validation Behavior</th>
</tr>
</thead>
<tbody><tr>
<td>Cisco IOS-XR</td>
<td><code>interface GigabitEthernet0/0/0/0</code></td>
<td>Commit-based</td>
<td>Fails at commit</td>
</tr>
<tr>
<td>Junos</td>
<td><code>set interfaces ge-0/0/0</code></td>
<td>Candidate + Commit</td>
<td>Commit check available</td>
</tr>
<tr>
<td>Nokia SROS</td>
<td><code>/configure router interface &lt;name&gt; port 1/1/c1/1</code></td>
<td>Candidate + Commit</td>
<td>Validate check available</td>
</tr>
</tbody></table>
<div>
<div>💡</div>
<div><em>Key insight: You cannot know whether a change went wrong unless you know exactly what the network looked like before the change was applied.</em></div>
</div>

<h2>Real Case 1: Interface Description Template Gone Wrong</h2>
<h3>Scenario</h3>
<p>You generate this Jinja template:</p>
<pre><code class="language-python">interface {{ interface_name }}
 description {{ description }}
 ip address {{ ip_address }} {{ mask }}
</code></pre>
<p>It works for:</p>
<ul>
<li>Cisco IOS-XR</li>
</ul>
<p>But your Junos device expects:</p>
<pre><code class="language-python">set interfaces ge-0/0/0 description "Uplink to Core"
set interfaces ge-0/0/0 unit 0 family inet address 10.1.1.1/24
</code></pre>
<h2>Real Case 2: BGP Policy Mismatch Across Vendors</h2>
<h3>Scenario</h3>
<p>You template BGP policies for 50 devices.</p>
<p>Your data model:</p>
<pre><code class="language-python">local_as: 65001
neighbor: 10.0.0.2
remote_as: 65002 
</code></pre>
<h3>Problem</h3>
<p>On IOS-XR:</p>
<pre><code class="language-python">router bgp 65001
 neighbor 10.0.0.2
  remote-as 65002
</code></pre>
<p>On Junos:</p>
<pre><code class="language-python">set protocols bgp group EBGP neighbor 10.0.0.2 peer-as 65002
</code></pre>
<p>But your automation mistakenly renders:</p>
<pre><code class="language-python">remote-as 65001
</code></pre>
<h3>Result?</h3>
<ul>
<li><p>Session never comes up</p>
</li>
<li><p>No routing exchange</p>
</li>
<li><p>Silent failure</p>
</li>
</ul>
<h2>Types of Validation</h2>
<ol>
<li><h3>Template Validation</h3>
</li>
</ol>
<p>Before pushing configurations, validate:</p>
<ul>
<li><p>YAML variables</p>
</li>
<li><p>Required fields</p>
</li>
<li><p>Data structure integrity</p>
</li>
</ul>
<p><strong>Example Python Validation</strong></p>
<pre><code class="language-python">import yaml
from jinja2 import Template, TemplateError

errors = []

def validate(data_text, template_text):
    try:
        yaml.safe_load(data_text)
    except yaml.YAMLError as e:
        errors.append(f"YAML error: {e}")

    try:
        Template(template_text)
    except TemplateError as e:
        errors.append(f"Template error: {e}")

    return errors
</code></pre>
<ol>
<li><h3>Syntax Validation</h3>
</li>
</ol>
<p>Each vendor supports some form of pre-check.</p>
<p><strong>Nokia SROS Model Driven - Validate</strong></p>
<pre><code class="language-plaintext">validate
</code></pre>
<p><strong>Junos – Commit Check</strong></p>
<pre><code class="language-python">commit check 
</code></pre>
<p><strong>Cisco IOS-XR – Commit Replace (Validate Before Apply)</strong></p>
<pre><code class="language-python">commit replace
</code></pre>
<ol>
<li><h3>Logical Validation</h3>
</li>
</ol>
<p>Syntax may pass. But logic may be wrong.</p>
<p>Examples:</p>
<ul>
<li><p>Local AS equals Remote AS in eBGP</p>
</li>
<li><p>IP address overlaps existing subnet</p>
</li>
<li><p>Duplicate loopback</p>
</li>
<li><p>MTU mismatch across link</p>
</li>
</ul>
<p><strong>Example Logical Check in Python</strong></p>
<pre><code class="language-python">if data["local_as"] == data["remote_as"]:
    raise ValueError("Local AS and Remote AS cannot be equal in eBGP")
</code></pre>
<ol>
<li><h3>Pre- and Post-Change Checks</h3>
</li>
</ol>
<p>Before applying config:</p>
<ul>
<li><p>Is interface already configured?</p>
</li>
<li><p>Does BGP session already exist?</p>
</li>
<li><p>Is policy already attached?</p>
</li>
<li><p>Is ISIS adjacency healthy?</p>
</li>
</ul>
<p>This ensures:</p>
<blockquote>
<p>One must avoid implementing new changes on an already broken network.</p>
</blockquote>
<p>After applying config:</p>
<ul>
<li><p>Check BGP state = Established</p>
</li>
<li><p>Check ISIS adjacency = Up</p>
</li>
<li><p>Check route present in RIB</p>
</li>
<li><p>Ping test</p>
</li>
</ul>
<h1>Safe Change Workflow in Multi-Vendor Networks</h1>
<p>Here's the workflow:</p>
<pre><code class="language-python">          Inventory Data (YAML)
                    │
            Jinja Rendering
                    │
         Template + YAML Validation
                    │
          Vendor Syntax Check
                    │
          Logical Validation
                    │
          Pre-State Snapshot
                    │
              Deployment
                    │
          Post-State Verification
                    │
            Auto Rollback (if fail)
</code></pre>
<h1><strong>Config Builder Tool</strong></h1>
<h2>About this Project</h2>
<p>I began my network automation journey in 2018, seeking more efficient ways to perform my job. Initially, everything was manual—logging into devices individually, typing <code>show</code> commands, copying configurations, and hoping nothing would break during a 2 AM change window. Then, I discovered a course by David Bombal "<strong>Python Network Programming for Network Engineers</strong>", which transformed my approach, and I haven't looked back since.</p>
<p>The <strong>Config Builder Tool</strong> was one of my first complete project, allowing me to demonstrate to my colleagues the power of <em>network automation.</em> This project is designed to automate the generation of network configurations using Python and Jinja2. The tool has been tested with Nokia SROS routers running releases 22.10.R1 and 24.10.R5, operating in model-driven mode. It utilizes NAPALM (<strong>Network Automation and Programmability Abstraction Layer with Multivendor support</strong>) to automate interactions with network devices via NETCONF.</p>
<p>NAPALM offers a configuration method called <code>compare_config</code>, which compares the candidate and running configurations on the SROS target. This is useful for <strong>pre-checking</strong> before deploying any configurations. This tool is interactive the user will be given a prompt to apply the configurations if required.</p>
<h2>Prerequisites</h2>
<h3>Step 1</h3>
<p>Enable MD-CLI on the Nokia SR OS network Device</p>
<pre><code class="language-plaintext">A:R1# /configure system management-interface cli md-cli auto-config-save
A:R1# /configure system management-interface configuration-mode model-driven
</code></pre>
<h3>Step 2</h3>
<p>Enable NETCONF on the Nokia SR OS network Device</p>
<pre><code class="language-plaintext">(gl)[configure system management-interface]
A:admin@R1#
    netconf {
        admin-state enable
        auto-config-save true
    }
</code></pre>
<p>Select YANG models to use on the Nokia SR OS network Device</p>
<pre><code class="language-plaintext">(gl)[configure system management-interface]
A:admin@R1#
    yang-modules {
       nokia-modules false
       nokia-combined-modules true
    }
</code></pre>
<p>Select NETCONF user and permissions</p>
<pre><code class="language-plaintext">(gl)[configure local-user system security user-params]
A:admin@R1#
 {
        user “admin" {
            password “admin"
            access {
                netconf true
            }
            console {
                member ["administrative"]
            }
        }
    }
</code></pre>
<h3>Step 3</h3>
<p>Before you begin, ensure you have the following installed:</p>
<ol>
<li><p>Python 3.x</p>
</li>
<li><p>NAPALM library</p>
</li>
<li><p>PyYAML and Jinja2 libraries</p>
</li>
</ol>
<h2>Repository Structure</h2>
<pre><code class="language-plaintext">├── vars.yml
├── builder.py
└── servicevpls.xml.j2
</code></pre>
<h2>Usage</h2>
<ol>
<li><strong>File</strong>: <code>vars.yml</code></li>
</ol>
<pre><code class="language-python">vpls:
  - { name: demo_vpls1, id: 80, saps: [1/1/c3/2:15 , 1/1/c3/3:16] }
  - { name: demo_vpls2, id: 90, saps: [] }
  - { name: demo_vpls3, id: 100, saps: ["1/1/c3/4:17", "1/1/c4/2:18", "1/1/c4/2:19"] }
auto:
  start: 2000000000
  end: 2147483647
</code></pre>
<ol>
<li><strong>File</strong>: <code>servicevpls.xmls.j2</code></li>
</ol>
<pre><code class="language-python">&lt;configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"&gt;
        &lt;service&gt;
            &lt;customer&gt;
                &lt;customer-name&gt;demo&lt;/customer-name&gt;
                &lt;description&gt;NETCONF L2VPN demo Nokia SR OS&lt;/description&gt;
                &lt;contact&gt;DEVOps Team&lt;/contact&gt;
            &lt;/customer&gt;
            &lt;md-auto-id&gt;
                &lt;service-id-range&gt;
                    &lt;start&gt;{{ auto.start }}&lt;/start&gt;
                    &lt;end&gt;{{ auto.end }}&lt;/end&gt;
                &lt;/service-id-range&gt;
                &lt;customer-id-range&gt;
                    &lt;start&gt;{{ auto.start }}&lt;/start&gt;
                    &lt;end&gt;{{ auto.end }}&lt;/end&gt;
                &lt;/customer-id-range&gt;
            &lt;/md-auto-id&gt;
{% for svc in vpls %}
          &lt;vpls&gt;
            &lt;service-name&gt;{{ svc.name }}&lt;/service-name&gt;
            &lt;customer&gt;demo&lt;/customer&gt;
            &lt;admin-state&gt;enable&lt;/admin-state&gt;
{% for sap in svc.saps %}
            &lt;sap&gt;
              &lt;sap-id&gt;{{ sap }}&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
{% endfor %}
          &lt;/vpls&gt;
{% endfor %}
        &lt;/service&gt;
&lt;/configure&gt;
</code></pre>
<ol>
<li><strong>Main Script</strong></li>
</ol>
<pre><code class="language-python">import sys
import json
from jinja2 import Environment, FileSystemLoader
from napalm import get_network_driver

#Import YAML from PyYAML
import yaml

def configbuilder ():
    print('''

****************************************************
CONFIG BUILDER TOOL
****************************************************
    ''')

configbuilder()

if len(sys.argv) == 3:
    #Load data from YAML file into Python dictionary
    config = yaml.load(open(sys.argv[1]), Loader=yaml.FullLoader)

    #Load Jinja2 template
    env = Environment(loader = FileSystemLoader('./'), trim_blocks=True, lstrip_blocks=True)
    template = env.get_template(sys.argv[2])

    #Render template using data and print the output
    print('''

****************************************************
PRE-LOADED CONFIGS
****************************************************
    
    ''')
    print(template.render(config))
    
else:

    print("Usage: python3 builder.py &lt;data_yml_file&gt; &lt;jinja_template_file&gt;")
    print()
    print()
    sys.exit();
    
# Return template with data and store it into variable

response = template.render(config)

# Connect to the router and merge config

hostname = '172.20.20.13'
username = 'username'
password = 'password'

def connect ():
   """ This function is used to connect into the network device and prompt a user to commit or discard changes """
   driver = get_network_driver('sros')
   device = driver(hostname=hostname, username=username, password=password)
   device.open()
   device.load_merge_candidate(config=response)

   print()
   print()
   print('''

***************************************************************************
COMPARE AND LOAD CONFIGS
***************************************************************************
         ''')

   print("Results will be displayed when the is a DIFFERENCE between running and candidate configurations ")
   print()
   print("No Results will be displayed if running and candidate have the same configurations")
   print()
   print(device.compare_config())
   print()
   # for json format use the below option
   #print(device.compare_config('json_format':True))
     
   try:
       choice = input("\nWould you like to commit these changes? [y or N]: ")
   except NameError:
       choice = input("\nWould you like to commit these changes? [y or N]: ")
    
   if choice == "y":
       print()
       print(f"Committing the configurations on router {hostname} as {username}")
       print()
       device.commit_config()
   else:
       print()
       print(f"Discarding and not applying the configurations on router {hostname} as {username}")
       print()
       device.discard_config()
       
connect()
</code></pre>
<ol>
<li><strong>Expected Output</strong></li>
</ol>
<p>Rendered configuration template</p>
<pre><code class="language-python">****************************************************
PRE-LOADED CONFIGS
****************************************************


&lt;configure xmlns="urn:nokia.com:sros:ns:yang:sr:conf"&gt;
        &lt;service&gt;
            &lt;customer&gt;
                &lt;customer-name&gt;demo&lt;/customer-name&gt;
                &lt;description&gt;NETCONF L2VPN demo Nokia SR OS&lt;/description&gt;
                &lt;contact&gt;DEVOps Team&lt;/contact&gt;
            &lt;/customer&gt;
            &lt;md-auto-id&gt;
                &lt;service-id-range&gt;
                    &lt;start&gt;2000000000&lt;/start&gt;
                    &lt;end&gt;2147483647&lt;/end&gt;
                &lt;/service-id-range&gt;
                &lt;customer-id-range&gt;
                    &lt;start&gt;2000000000&lt;/start&gt;
                    &lt;end&gt;2147483647&lt;/end&gt;
                &lt;/customer-id-range&gt;
            &lt;/md-auto-id&gt;
          &lt;vpls&gt;
            &lt;service-name&gt;demo_vpls1&lt;/service-name&gt;
            &lt;customer&gt;demo&lt;/customer&gt;
            &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;sap&gt;
              &lt;sap-id&gt;1/1/c1/2:15&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
            &lt;sap&gt;
              &lt;sap-id&gt;1/1/c1/3:16&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
          &lt;/vpls&gt;
          &lt;vpls&gt;
            &lt;service-name&gt;demo_vpls2&lt;/service-name&gt;
            &lt;customer&gt;demo&lt;/customer&gt;
            &lt;admin-state&gt;enable&lt;/admin-state&gt;
          &lt;/vpls&gt;
          &lt;vpls&gt;
            &lt;service-name&gt;demo_vpls3&lt;/service-name&gt;
            &lt;customer&gt;demo&lt;/customer&gt;
            &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;sap&gt;
              &lt;sap-id&gt;1/1/c1/4:17&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
            &lt;sap&gt;
              &lt;sap-id&gt;1/1/c2/2:18&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
            &lt;sap&gt;
              &lt;sap-id&gt;1/1/c2/3:19&lt;/sap-id&gt;
              &lt;admin-state&gt;enable&lt;/admin-state&gt;
            &lt;/sap&gt;
          &lt;/vpls&gt;
        &lt;/service&gt;
&lt;/configure&gt;
</code></pre>
<p>Comparison candidate vs running configuration</p>
<div>
<div>💡</div>
<div>The is no L2VPN (VPLS) services on this router therefore you will see only the new addition</div>
</div>

<pre><code class="language-python">***************************************************************************
COMPARE AND LOAD CONFIGS

***************************************************************************

Results will be displayed when the is a DIFFERENCE between running and candidate configurations

No Results will be displayed if running and candidate have the same configurations

[
    "add",
    "configure.service",
    [
        [
            "customer",
            {
                "contact": "DEVOps Team",
                "customer-name": "demo",
                "description": "NETCONF L2VPN demo Nokia SR OS"
            }
        ],
        [
            "md-auto-id",
            {
                "customer-id-range": {
                    "end": "2147483647",
                    "start": "2000000000"
                },
                "service-id-range": {
                    "end": "2147483647",
                    "start": "2000000000"
                }
            }
        ],
        [
            "vpls",
            [
                {
                    "admin-state": "enable",
                    "customer": "demo",
                    "sap": [
                        {
                            "admin-state": "enable",
                            "sap-id": "1/1/c1/2:15"
                        },
                        {
                            "admin-state": "enable",
                            "sap-id": "1/1/c1/3:16"
                        }
                    ],
                    "service-name": "demo_vpls1"
                },
                {
                    "admin-state": "enable",
                    "customer": "demo",
                    "service-name": "demo_vpls2"
                },
                {
                    "admin-state": "enable",
                    "customer": "demo",
                    "sap": [
                        {
                            "admin-state": "enable",
                            "sap-id": "1/1/c1/4:17"
                        },
                        {
                            "admin-state": "enable",
                            "sap-id": "1/1/c2/2:18"
                        },
                        {
                            "admin-state": "enable",
                            "sap-id": "1/1/c2/3:19"
                        }
                    ],
                    "service-name": "demo_vpls3"
                }
            ]
        ]
    ]
]
</code></pre>
<p>Device commit prompt</p>
<pre><code class="language-python">Would you like to commit these changes? [y or N]:

Would you like to commit these changes? [y or N]: N

Discarding and not applying the configurations on router 172.20.20.13 as admin
</code></pre>
<h1>Key Takeaways</h1>
<ul>
<li><p>Never deploy untested configurations</p>
</li>
<li><p>Use multiple validation layers</p>
</li>
<li><p>Always take pre-deployment backups</p>
</li>
<li><p>Capture pre/post state</p>
</li>
<li><p>Have rollback procedures ready</p>
</li>
<li><p>Use vendor-native commit checks</p>
</li>
</ul>
<h1>Download the Code</h1>
<p>All templates and script: <a href="https://gitlab.com/kgosileburu/configbuilder-tool">configbuildertool</a></p>
<h1>Final Thoughts</h1>
<blockquote>
<p>If Jinja gives you power…</p>
<p>Validation gives you safety.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Mastering Dynamic Configurations: A Beginner's Guide to Jinja2 - Part 2]]></title><description><![CDATA[Having completed Part 1, you now understand Jinja2 and its basic templating constructs, including variables, control structures, and simple filters for generating dynamic text. In Part 2, we will delv]]></description><link>https://codednetwork.com/mastering-dynamic-configurations-a-beginner-s-guide-to-jinja2-part-2</link><guid isPermaLink="true">https://codednetwork.com/mastering-dynamic-configurations-a-beginner-s-guide-to-jinja2-part-2</guid><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 03 Mar 2026 07:52:26 GMT</pubDate><enclosure url="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/68933f4690103a1d4a8d7df7/dd1571d3-13a0-4248-8e42-16811e1f83b8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Having completed Part 1, you now understand Jinja2 and its basic templating constructs, including variables, control structures, and simple filters for generating dynamic text. In Part 2, we will delve deeper into practical techniques that make Jinja2 a powerful tool for configuration management. You can look forward to detailed, practical examples on creating maintainable, reusable templates for router configurations..</p>
<h2>Use Case 1: Nokia SR OS Service Configuration</h2>
<h3>Scenario</h3>
<p>You are deploying 50 new customer L3VPN services on Nokia SR OS routers. Each customer requires:</p>
<ul>
<li><p>Unique customer ID</p>
</li>
<li><p>Service description</p>
</li>
<li><p>IP interfaces</p>
</li>
<li><p>BGP Peering</p>
</li>
</ul>
<h3>Prerequisites</h3>
<pre><code class="language-python">pip install jinja2
pip install pyyaml 
pip install netmiko #For deployment (optional)

# Install required libraries t 
# It is always essential create a python environment
</code></pre>
<h3>Jinja2 Template</h3>
<p><strong>File</strong>: <code>nokia_customer_service.j2</code></p>
<pre><code class="language-python">{# Nokia SR OS Customer Service Template #}
{# =================================== #}
{# Customer: {{ customer.name }} #}
{# Date: {{ timestamp }} #}
{# =================================== #}

/configure
#--------------------------------------------------
echo "Creating Customer {{ customer.id }}: {{ customer.name }}"
#--------------------------------------------------

    service
        customer {{ customer.id }} create
            description "{{ customer.name }}"
        exit
        
        {% for vprn in customer.vprns %}
        vprn {{ vprn.service_id }} customer {{ customer.id }} create
            service-name "{{ vprn.name }}"
            description "{{ vprn.description }}"
            autonomous-system {{ vprn.as_number }}
            route-distinguisher {{ vprn.rd }}
            
            {# Configure VPRN Interfaces #}
            {% for interface in vprn.interfaces %}
            interface "{{ interface.name }}" create
                address {{ interface.ip }}/{{ interface.prefix }}
                sap {{ interface.sap }} create
                    description "{{ interface.description }}"
                exit
            exit
            {% endfor %}
            
            {# Configure BGP if present #}
            {% if vprn.bgp %}
            bgp
                group "{{ vprn.bgp.group_name }}"
                    type {{ vprn.bgp.type }}
                    peer-as {{ vprn.bgp.peer_as }}
                    
                    {% for neighbor in vprn.bgp.neighbors %}
                    neighbor {{ neighbor.ip }}
                        description "{{ neighbor.description }}"
                    exit
                    {% endfor %}
                exit
            exit
            {% endif %}
            
            no shutdown
        exit
        {% endfor %}
    exit
exit
</code></pre>
<h3>Data YAML file</h3>
<p><strong>File:</strong> <code>customers_data.yaml</code></p>
<pre><code class="language-yaml">---
customers:
  - id: 100
    name: CodedNetwork Corp
    vprns:
      - service_id: 1000
        name: CodedN-CORP-VPRN
        description: Enterprise Corp Main VPRN
        as_number: 65000
        rd: 65000:100
        interfaces:
          - name: to-customer-site-1
            ip: 10.100.1.1
            prefix: 30
            sap: 1/1/c1/1:100
            description: Customer Site 1 Connection
          - name: to-customer-site-2
            ip: 10.100.2.1
            prefix: 30
            sap: 1/1/c2/1:101
            description: Customer Site 2 Connection
        bgp:
          group_name: customer-bgp
          type: external
          peer_as: 65100
          neighbors:
            - ip: 10.100.1.2
              description: Site 1 CE Router
            - ip: 10.100.2.2
              description: Site 2 CE Router
  - id: 101
    name: Tech Startup Inc
    vprns:
      - service_id: 1001
        name: TECH-STARTUP-VPRN
        description: Tech Startup Main VPRN
        as_number: 65001
        rd: 65000:101
        interfaces:
          - name: to-startup-hq
            ip: 10.101.1.1
            prefix: 30
            sap: 1/1/c3/1:200
            description: Startup HQ Connection
        bgp:
          group_name: startup-bgp
          type: external
          peer_as: 65200
          neighbors:
            - ip: 10.101.1.2
              description: HQ CE Router
</code></pre>
<h2>Nokia SR OS Configuration Generator - Code Overview</h2>
<pre><code class="language-python">#!/usr/bin/env python3
"""
Nokia SR OS Configuration Generator using Jinja2
Generates customer service configurations from YAML data
"""

from jinja2 import Environment, FileSystemLoader
import yaml
from datetime import datetime
import os

def load_data(yaml_file):
    """Load customer data from YAML file"""
    with open(yaml_file, 'r') as f:
        return yaml.safe_load(f)

def generate_config(template_file, data, output_dir='configs_generated'):
    """
    Generate configurations from template and data
    
    Args:
        template_file: Jinja2 template file path
        data: Dictionary with customer data
        output_dir: Directory to save generated configs
    """
    
    # Create output directory
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # Set up Jinja2 environment
    env = Environment(
        loader=FileSystemLoader('.'),
        trim_blocks=True,
        lstrip_blocks=True
    )
    
    # Load template
    template = env.get_template(template_file)
    
    # Generate config for each customer
    for customer in data['customers']:
        # Add timestamp to data
        customer['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        # Render template with customer data
        config = template.render(customer=customer)
        
        # Create filename
        filename = f"{output_dir}/nokia_customer_{customer['id']}_{customer['name'].replace(' ', '_')}.cfg"
        
        # Save to file
        with open(filename, 'w') as f:
            f.write(config)
        
        print(f"✓ Generated: {filename}")
        print(f"  Customer: {customer['name']} (ID: {customer['id']})")
        print(f"  VPRNs: {len(customer['vprns'])}")
        print()

def main():
    """Main execution"""
    print("="*70)
    print("Nokia SR OS Configuration Generator")
    print("="*70)
    print()
    
    # Load data
    print("Loading customer data...")
    data = load_data('customers_data.yaml')
    print(f"✓ Loaded {len(data['customers'])} customers")
    print()
    
    # Generate configurations
    print("Generating configurations...")
    print()
    generate_config('nokia_customer_service.j2', data)
    
    print("="*70)
    print("Generation complete!")
    print("="*70)

if __name__ == "__main__":
    main()
</code></pre>
<h2>Detailed Step-by-Step Explanation</h2>
<ol>
<li>Imports and Setup</li>
</ol>
<pre><code class="language-python">from jinja2 import Environment, FileSystemLoader
import yaml
from datetime import datetime
import os
</code></pre>
<p><strong>Purpose of each Import</strong></p>
<ul>
<li><p><strong>Jinja2</strong> : Template engine for generating text files</p>
</li>
<li><p><strong>YAML</strong> : Reads structured customer data</p>
</li>
<li><p><strong>datetime</strong> : Adds Timestamps to configs</p>
</li>
<li><p><strong>os</strong> : Creates directories for output files</p>
</li>
</ul>
<ol>
<li><code>load_data()</code> Function</li>
</ol>
<pre><code class="language-python">def load_data(yaml_file):
    with open(yaml_file, 'r') as f:
        return yaml.safe_load(f)

# The function reads a YAML file and returns its contents as a Python dictionary
</code></pre>
<ol>
<li><code>generate_config()</code> Function</li>
</ol>
<pre><code class="language-python">if not os.path.exists(output_dir):
    os.makedirs(output_dir)

# Creates a folder called `generated_configs` if it doesn't exist
</code></pre>
<pre><code class="language-python">env = Environment(
    loader=FileSystemLoader('.'),
    trim_blocks=True,
    lstrip_blocks=True
)

# Sets up template engine to look for templates in current directory
#`trim_blocks` and `lstrip_blocks` remove extra whitespace for cleaner output
</code></pre>
<pre><code class="language-python">template = env.get_template(template_file 

# Loads the Jinja2 template file (contains Nokia router config structure with placeholders)
</code></pre>
<pre><code class="language-python">for customer in data['customers']:
    customer['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    config = template.render(customer=customer)

# Loops through each customer in the YAML data
# Adds current timestamp to customer data
# Renders the template by replacing placeholders with actual customer values
</code></pre>
<pre><code class="language-python">filename = f"{output_dir}/nokia_customer_{customer['id']}_{customer['name'].replace(' ', '_')}.cfg"
with open(filename, 'w') as f:
    f.write(config)

# Writes the rendered configuration to file
</code></pre>
<ol>
<li><code>main ()</code> Function</li>
</ol>
<pre><code class="language-python">def main():
    data = load_data('customers_data.yaml')
    generate_config('nokia_customer_service.j2', data)

# Loads customer data from YAML file
# Generates all configuration files
</code></pre>
<h3>To run the script</h3>
<pre><code class="language-python">python3 nokia_config_generator.py 
</code></pre>
<h3>Output:</h3>
<pre><code class="language-python">python3 nokia_config_generator.py 

======================================================================
Nokia SR OS Configuration Generator
======================================================================

Loading customer data...
✓ Loaded 2 customers

Generating configurations...

✓ Generated: configs_generated/nokia_customer_100_CodedNetwork_Corp.cfg
  Customer: CodedNetwork Corp (ID: 100)
  VPRNs: 1

✓ Generated: configs_generated/nokia_customer_101_Tech_Startup_Inc.cfg
  Customer: Tech Startup Inc (ID: 101)
  VPRNs: 1

======================================================================
Generation complete!
======================================================================

#generated configuration under configs_generated folder 

ls -l
nokia_customer_100_CodedNetwork_Corp.cfg
nokia_customer_101_Tech_Startup_Inc.cfg
</code></pre>
<h3>Generated Configuration Example</h3>
<p><strong>File:</strong> <code>nokia_customer_100_CodedNetwork_Corp.cfg</code></p>
<pre><code class="language-python">cat nokia_customer_100_CodedNetwork_Corp.cfg 

/configure
#--------------------------------------------------
echo "Creating Customer 100: CodedNetwork Corp"
#--------------------------------------------------

    service
        customer 100 create
            description "CodedNetwork Corp"
        exit
        
        vprn 1000 customer 100 create
            service-name "CodedN-CORP-VPRN"
            description "Enterprise Corp Main VPRN"
            autonomous-system 65000
            route-distinguisher 65000:100
            
            interface "to-customer-site-1" create
                address 10.100.1.1/30
                sap 1/1/c1/1:100 create
                    description "Customer Site 1 Connection"
                exit
            exit
            interface "to-customer-site-2" create
                address 10.100.2.1/30
                sap 1/1/c2/1:101 create
                    description "Customer Site 2 Connection"
                exit
            exit
            
            bgp
                group "customer-bgp"
                    type external
                    peer-as 65100
                    
                    neighbor 10.100.1.2
                        description "Site 1 CE Router"
                    exit
                    neighbor 10.100.2.2
                        description "Site 2 CE Router"
                    exit
                exit
            exit
            
            no shutdown
        exit
    exit
	
</code></pre>
<h2>Use Case 2: Nokia SR OS Interface Configuration</h2>
<h3>Scenario</h3>
<p>Configure 48 access ports on a Nokia 7750 SR with:</p>
<ul>
<li><p>Different VLAN assignments</p>
</li>
<li><p>Port descriptions based on location</p>
</li>
<li><p>Speed and duplex settings</p>
</li>
</ul>
<h3>Jinja2 Template</h3>
<p><strong>File</strong>: <code>nokia_interface_config.j2</code></p>
<pre><code class="language-python">{# Nokia SR OS Interface Configuration Template #}
/configure
{% for interface in interfaces %}
    port {{ interface.port }} create
        description "{{ interface.description }}"
        ethernet
            mode {{ interface.mode }}
            {% if interface.speed %}
            speed {{ interface.speed }}
            {% endif %}
            {% if interface.duplex %}
            duplex {{ interface.duplex }}
            {% endif %}
            encap-type {{ interface.encap_type|default('dot1q') }}
        exit
        no shutdown
    exit
{% endfor %}
exit
</code></pre>
<h3>Data YAML file</h3>
<p><strong>File:</strong> <code>interfaces_data.yaml</code></p>
<pre><code class="language-python">interfaces:
  - port: "1/1/c1/1"
    description: "Building A - Floor 1 - Room 101"
    mode: "access"
    speed: "1000"
    duplex: "full"
    vlan: 100
    
  - port: "1/1/c2/1"
    description: "Building A - Floor 1 - Room 102"
    mode: "access"
    speed: "1000"
    duplex: "full"
    vlan: 100
    
  - port: "1/1/c3/1"
    description: "Building A - Floor 2 - Conference Room"
    mode: "access"
    speed: "1000"
    duplex: "full"
    vlan: 200
    
  - port: "1/1/c4/1"
    description: "Uplink to Core Switch"
    mode: "network"
    speed: "10000"
    encap_type: "qinq"
</code></pre>
<h3>Output:</h3>
<pre><code class="language-python">python3 nokia_config_interface_gen.py 

======================================================================
Nokia SR OS Configuration Generator
======================================================================

Loading interface data...
✓ Loaded 4 interfaces

Generating configurations...

✓ Generated: configs_generated/nokia_interface_Building_A_-_Floor_1_-_Room_101.cfg

✓ Generated: configs_generated/nokia_interface_Building_A_-_Floor_1_-_Room_102.cfg

✓ Generated: configs_generated/nokia_interface_Building_A_-_Floor_2_-_Conference_Room.cfg

✓ Generated: configs_generated/nokia_interface_Uplink_to_Core_Switch.cfg

======================================================================
Generation complete!
======================================================================
</code></pre>
<h2>Use Case 3: Multi-vendor BGP Configuration</h2>
<h3>Scenario</h3>
<p>Generate BGP configuration for Nokia SR OS, Cisco IOS XR, and Juniper routers.</p>
<h3>Nokia SROS Template</h3>
<p><strong>File:</strong> <code>nokia_bgp.j2</code></p>
<pre><code class="language-python">/configure
    router bgp
        autonomous-system {{ bgp.local_as }}
        {% for neighbor in bgp.neighbors %}
        neighbor {{ neighbor.ip }}
            peer-as {{ neighbor.remote_as }}
            description "{{ neighbor.description }}"
            family ipv4
            {% if neighbor.password %}
            authentication-key "{{ neighbor.password }}"
            {% endif %}
        exit
        {% endfor %}
    exit
exit
</code></pre>
<h3>Cisco IOS XR Template</h3>
<p><strong>File:</strong> <code>cisco_bgp.j2</code></p>
<pre><code class="language-python">router bgp {{ bgp.local_as }}
{% for neighbor in bgp.neighbors %}
 neighbor {{ neighbor.ip }}
  remote-as {{ neighbor.remote_as }}
  description {{ neighbor.description }}
  address-family ipv4 unicast
  !
  {% if neighbor.password %}
  password encrypted {{ neighbor.password }}
  {% endif %}
 !
{% endfor %}
!
</code></pre>
<h3>Juniper Template</h3>
<p><strong>File:</strong> <code>juniper_bgp.j2</code></p>
<pre><code class="language-python">protocols {
    bgp {
        group external {
            type external;
            local-as {{ bgp.local_as }};
            {% for neighbor in bgp.neighbors %}
            neighbor {{ neighbor.ip }} {
                description "{{ neighbor.description }}";
                peer-as {{ neighbor.remote_as }};
                {% if neighbor.password %}
                authentication-key "{{ neighbor.password }}";
                {% endif %}
            }
            {% endfor %}
        }
    }
}
</code></pre>
<h3>Unified YAML data file</h3>
<p><strong>File:</strong> <code>bgp_data.yaml</code></p>
<pre><code class="language-python">bgp:
  local_as: 65000
  neighbors:
    - ip: "10.1.1.1"
      remote_as: 65001
      description: "Peer to ISP1"
      password: "secret123"
    
    - ip: "10.1.1.5"
      remote_as: 65002
      description: "Peer to ISP2"
      password: "secret123"
</code></pre>
<h3>Multivendor Python Script</h3>
<p><strong>File:</strong> <code>generate_multivendor.py</code></p>
<pre><code class="language-python">from jinja2 import Environment, FileSystemLoader
import yaml
import os

VENDORS = {
    'nokia': 'nokia_bgp.j2',
    'cisco': 'cisco_bgp.j2',
    'juniper': 'juniper_bgp.j2'
}

def generate_multivendor_config(data_file, output_dir='multivendor_configs'):
    """Generate configs for all vendors from same data"""
    
    # Load data
    with open(data_file, 'r') as f:
        data = yaml.safe_load(f)
    
    # Create output directory
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    
    # Set up Jinja2
    env = Environment(loader=FileSystemLoader('.'))
    
    # Generate for each vendor
    for vendor, template_file in VENDORS.items():
        template = env.get_template(template_file)
        config = template.render(**data)
        
        filename = f"{output_dir}/{vendor}_bgp_config.cfg"
        with open(filename, 'w') as f:
            f.write(config)
        
        print(f"✓ Generated {vendor.upper()} configuration: {filename}")

if __name__ == "__main__":
    generate_multivendor_config('bgp_data.yaml')
</code></pre>
<h3>Output:</h3>
<pre><code class="language-python">python3 generate_multivendor.py 

✓ Generated NOKIA configuration: multivendor_configs/nokia_bgp_config.cfg
✓ Generated CISCO configuration: multivendor_configs/cisco_bgp_config.cfg
✓ Generated JUNIPER configuration:   multivendor_configs/juniper_bgp_config.cfg
</code></pre>
<h1>Putting It All Together: Complete Workflow</h1>
<h2>Step 1: Create Templates</h2>
<p>Create template files for each configuration type</p>
<h2>Step 2: Prepare Data</h2>
<p>Create YAML files with customer/site/service data</p>
<h2>Step 3: Generate Configurations</h2>
<pre><code class="language-python">python3 generate_multivendor.py
</code></pre>
<h2>Step 4: Review Generated Configs</h2>
<h1>Key Takeaways</h1>
<ul>
<li><p><strong>Separation of Responsibilities -</strong> Templates (structure) vs Data (content)</p>
</li>
<li><p><strong>Reusability -</strong> One template, many configurations</p>
</li>
<li><p><strong>Consistency -</strong> Same structure every time</p>
</li>
<li><p><strong>Speed -</strong> Generate hundreds of configs in seconds</p>
</li>
<li><p><strong>Maintainability -</strong> Update template once, regenerate all configs</p>
</li>
</ul>
<h1>Download the Code</h1>
<p>All templates and scripts: <a href="https://gitlab.com/kgosileburu/network-automation-scripts">network-automation-week-3</a></p>
]]></content:encoded></item><item><title><![CDATA[Mastering Dynamic Configurations: A Beginner's Guide to Jinja2 - Part 1]]></title><description><![CDATA[In contemporary network automation, the ability to dynamically generate configurations is essential for network engineers operating in multi-vendor environments. Manual configuration is prone to error]]></description><link>https://codednetwork.com/mastering-dynamic-configurations-a-beginner-s-guide-to-jinja2-part-1</link><guid isPermaLink="true">https://codednetwork.com/mastering-dynamic-configurations-a-beginner-s-guide-to-jinja2-part-1</guid><category><![CDATA[Jinja2]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[YAML]]></category><category><![CDATA[junos]]></category><category><![CDATA[Cisco]]></category><category><![CDATA[nokia]]></category><category><![CDATA[Devops]]></category><category><![CDATA[NetworkAutomation]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 24 Feb 2026 07:04:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771379399135/7d90789d-8806-47a7-b7b9-8913f4df5648.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In contemporary network automation, the ability to dynamically generate configurations is essential for network engineers operating in multi-vendor environments. Manual configuration is prone to errors, lacks consistency, and is challenging to scale. Jinja2 templates serve as a powerful solution in this context. By integrating structured data (YAML/JSON) with reusable templates, network engineers can produce standardized and validated configurations across numerous devices in a safe and efficient manner.</p>
<p>This week, we are making significant progress by <strong>generating configurations dynamically</strong>. Rather than manually creating configuration files for each device, we will utilize <strong>Jinja2 templates</strong> to produce hundreds of configurations from a single data file.</p>
<p>In a <strong>real-world scenario</strong>, consider the task of configuring 100 new customer sites across Nokia SR OS, Cisco IOS XR, and Juniper routers. Each site requires unique VLANs, IP addresses, and routing configurations. Manually completing this task would take several days. However, using templates, it can be accomplished in just minutes.</p>
<h1>Why Choose Jinja2 for Network Automation?</h1>
<p><strong>Jinja2</strong> is a versatile and efficient templating engine commonly employed in automation frameworks like Ansible, as well as in custom Python automation scripts. It allows engineers to develop dynamic templates incorporating variables, loops, and conditional logic. This capability makes it particularly well-suited for generating configurations in multi-vendor environments.</p>
<h3>Think of it like this:</h3>
<ul>
<li><p>A mail merge in Microsoft Word</p>
</li>
<li><p>A form letter where you fill in the blanks</p>
</li>
<li><p>A recipe where you substitute ingredients</p>
</li>
</ul>
<h2>The Traditional Method (Manual Configuration)</h2>
<pre><code class="language-python"># Customer 1 - Nokia SROS router 
/configure service customer 100 create
description "Customer CodedNetwork Corp"
exit

# Customer 2 - Nokia SROS router 
/configure service customer 200 create
description "Lazyprogrammer INC"
exit

#...... Repeat for a 100 customers 😫
</code></pre>
<h2>The Modern Approach (Jinja2 Template)</h2>
<p><strong>Jinja2 Template:</strong></p>
<pre><code class="language-python"># Template only once !!! 🙂
/configure service customer {{ customer_id }} create
    description "{{ customer_name }}"
exit
</code></pre>
<p><strong>Data (YAML file):</strong></p>
<pre><code class="language-python">customers:
  - customer_id: 100
    customer_name: "Customer CodedNetwork Corp"
  - customer_id: 200
    customer_name: "Lazyprogrammer INC"
</code></pre>
<p><strong>Result</strong>: Generate 100 configurations in seconds.</p>
<h1>Understanding YAML</h1>
<p><strong>YAML (YAML Ain't Markup Language)</strong> is a human-readable data serialization format frequently utilized for configuration files and data exchange between languages with varying data structures. It is crafted to be straightforward to read and write, which makes it a favored option for configuration files in numerous applications, including network automation.</p>
<p>Key features of YAML include:</p>
<ol>
<li><p><strong>Simplicity</strong>: YAML is crafted to be straightforward to read and write, with a syntax that is more user-friendly compared to other data serialization formats like JSON or XML.</p>
</li>
<li><p><strong>Data Types</strong>: YAML supports a variety of data types, including scalars (strings, numbers, Booleans), lists, and dictionaries (also known as maps or hashes).</p>
</li>
<li><p><strong>Indentation</strong>: YAML uses indentation to denote the structure of data. The level of indentation indicates the hierarchy of data, similar to how Python uses indentation for code blocks.</p>
</li>
<li><p><strong>Comments</strong>: YAML permits comments, which can be added using the <code>#</code> symbol. This feature is beneficial for including explanations or notes within the configuration files.</p>
</li>
<li><p><strong>Compatibility</strong>: YAML is compatible with JSON, meaning that any valid JSON file is also a valid YAML file.</p>
</li>
</ol>
<p>In the context of <strong>Jinja2</strong> templates, YAML is frequently employed to store structured data that can be integrated into the templates to produce dynamic configurations. This approach facilitates a clear distinction between the data and the template logic, thereby simplifying the management and updating of configurations.</p>
<pre><code class="language-python"># Key-Value Pairs (Basic Structure)
hostname: core-r1
vendor: nokia
os: sros

# Lists( Arrays) 
interfaces:
  - name: ethernet-1/1
    ip: 10.0.0.1/24
  - name: ethernet-1/2
    ip: 10.0.1.1/24

# Nested Structures
device:
  hostname: core-r1
  vendor: nokia
  routing:
    bgp:
      asn: 65001
      router_id: 1.1.1.1

# Indentation (Critical Rule in YAML)
router:
  bgp:
    asn: 65001

# Strings, Integers, and Booleans
hostname: core-r1        # string
asn: 65001               # integer
enabled: true            # boolean

# Comments in YAML starts with "#"

hostname: core-r1
vendor: nokia  # SR OS platform
</code></pre>
<h2>Importance of YAML Validation</h2>
<p>A YAML validator is a tool or process that ensures a YAML file is syntactically correct, well-structured, and compliant before being utilized in automation pipelines. In network automation, where YAML is used for configuration generation, inventory, and deployments, validation serves as a <strong>critical safety layer</strong>.</p>
<h3>Prevents Automation Failures Prior to Deployment</h3>
<p>Automation tools such as Python scripts, Jinja2 templates, and Ansible heavily depend on YAML files. If the YAML is invalid, it can cause the entire pipeline to fail.</p>
<h3>Prevents Incorrect Configuration Generation (High Risk)</h3>
<p>When utilizing Jinja2 templates, YAML serves as the authoritative source. If the structure is incorrect, the templates may produce incomplete or erroneous configurations.</p>
<h3>Safeguards Production Networks Against Human Errors</h3>
<p>Most YAML errors result from human mistakes, including:</p>
<ul>
<li><p>Incorrect indentation</p>
</li>
<li><p>Duplicate keys</p>
</li>
<li><p>Missing quotes</p>
</li>
<li><p>Typographical errors</p>
</li>
</ul>
<p>In service provider environments, including core routers, software upgrades, and automation health checks, even a minor YAML error can lead to disruptions in:</p>
<ul>
<li><p>Routing policies</p>
</li>
<li><p>Interface provisioning</p>
</li>
<li><p>Telemetry pipelines</p>
</li>
</ul>
<p>A validator functions as a <strong>pre-change safety control</strong> before deployment</p>
<h3>Ensures Data Consistency Across Multi-Vendor Environments</h3>
<p>In multi-vendor automation environments (such as Nokia SR OS, Cisco IOS XR, and Junos), YAML is used to model device data. Validators ensure:</p>
<ul>
<li><p>Ensures correct schema structure</p>
</li>
<li><p>Verifies the presence of required fields (ASN, interfaces, hostname)</p>
</li>
<li><p>Confirms no missing attributes for specific vendors</p>
</li>
</ul>
<p>This is particularly crucial when creating vendor-specific configurations from a unified template.</p>
<h3>Supports CI/CD and Git-Based Automation Pipelines</h3>
<p>Modern network teams maintain YAML files in Git repositories to facilitate:</p>
<ul>
<li><p>Version control</p>
</li>
<li><p>Change tracking</p>
</li>
<li><p>Automated deployments</p>
</li>
</ul>
<p>Validation in CI/CD Pipelines:</p>
<pre><code class="language-bash">Git Commit → YAML Validation → Jinja2 Render → Lab Test → Production Deploy
</code></pre>
<p>If validation fails:</p>
<ul>
<li><p>Deployment is automatically halted</p>
</li>
<li><p>Prevents incorrect configurations from being applied to devices</p>
</li>
</ul>
<h3>Detects Indentation and Syntax Errors (A Common Issue)</h3>
<p>YAML is sensitive to indentation, requiring spaces only and not tabs.</p>
<p>A validator promptly identifies:</p>
<ul>
<li><p>Incorrect indentation</p>
</li>
<li><p>Invalid nesting</p>
</li>
<li><p>Structural inconsistencies</p>
</li>
</ul>
<h3>Enhances Automation Reliability and Scalability</h3>
<p>For large-scale automation involving hundreds of routers:</p>
<ul>
<li><p>Valid YAML ensures predictable automation behavior.</p>
</li>
<li><p>Invalid YAML leads to unpredictable failures.</p>
</li>
</ul>
<p>Benefits include:</p>
<ul>
<li><p>Stable template rendering</p>
</li>
<li><p>Faster troubleshooting</p>
</li>
<li><p>Cleaner automation logs</p>
</li>
<li><p>Reduced rollback scenarios</p>
</li>
</ul>
<h3>Essential for Jinja2 Template Rendering (Direct Impact)</h3>
<p>Jinja2 consumes YAML as input variables:</p>
<pre><code class="language-python">data = yaml.safe_load(open("devices.yaml"))
template.render(data)
</code></pre>
<h3>Security and Compliance Advantages</h3>
<p>Validated YAML helps prevent:</p>
<ul>
<li><p>Misconfigured access policies</p>
</li>
<li><p>Incorrect ACL deployments</p>
</li>
<li><p>Faulty automation scripts that push unsafe configurations</p>
</li>
</ul>
<p>My preferred YAML Validator is YAML Lint:</p>
<p><a href="https://www.yamllint.com/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1771464081173/8ed75ea5-41ee-42b7-a636-bc217621e474.png" alt="" style="display:block;margin:0 auto" /></a></p>
<h2>Best Practice Workflow for Your Automation Stack</h2>
<pre><code class="language-python">        YAML Inventory File
                |
                v
        YAML Validator (yamllint)
                |
        +-------+--------+
        |                |
      Valid            Invalid
        |                |
        v                v
   Jinja2 Templates   Fix Errors
        |
        v
 Config Generation → Device Deployment → Health Checks
</code></pre>
<h1>Jinja2 Basics</h1>
<h2>1. Variables</h2>
<p>Variables in Jinja2 are represented by double curly braces <code>{{ }}</code>. These act as placeholders, which are substituted with actual data during the template rendering process.</p>
<pre><code class="language-python">hostname {{ hostname }}
interface {{ interface_name }}
 ip address {{ ip_address }}
</code></pre>
<h2>2. Loops</h2>
<p>Loops enable you to iterate over lists such as interfaces, VLANs, BGP neighbors, or services. This functionality is particularly beneficial when configuring multiple interfaces or services on a routers</p>
<pre><code class="language-python">{% for intf in interfaces %}
interface {{ intf.name }}
 description {{ intf.description }}
 ip address {{ intf.ip }}
{% endfor %}
</code></pre>
<h2>3. Conditionals</h2>
<p>Conditionals incorporate logic into templates, enabling configurations to adapt dynamically based on factors such as vendor, device role, or feature flags. This is essential in multi-vendor environments where syntax varies across platforms..</p>
<pre><code class="language-python">{% if vendor == "cisco" %}
router bgp {{ asn }}
{% elif vendor == "nokia" %}
configure router bgp {{ asn }}
{% elif vendor == "juniper" %}
set protocols bgp group external type external
{% endif %}
</code></pre>
<h2>4. Filters</h2>
<p>Filters in Jinja2 are used to transform or format data before rendering it into the final configuration. They help clean, modify, or standardize values automatically.</p>
<h3>Common Networking Use Cases:</h3>
<ul>
<li><p>Uppercase interface names</p>
</li>
<li><p>Formatting IP addresses</p>
</li>
<li><p>Default values for missing fields</p>
</li>
</ul>
<pre><code class="language-python">hostname {{ hostname | upper }}
interface {{ interface | default("GigabitEthernet0/0") }}
</code></pre>
<h1>Advanced Jinja2 Features</h1>
<h2>1. Macros (Reusable Blocks)</h2>
<pre><code class="language-python">{# Define a macro #}
{% macro interface_config(port, vlan, description) %}
interface {{ port }}
    description "{{ description }}"
    vlan {{ vlan }}
    no shutdown
{% endmacro %}

{# Use the macro #}
{% for intf in interfaces %}
{{ interface_config(intf.port, intf.vlan, intf.desc) }}
{% endfor %}
</code></pre>
<h2>2. Include Other Templates</h2>
<pre><code class="language-python">{# base_template.j2 #}
/configure
    {% include 'system_config.j2' %}
    {% include 'interface_config.j2' %}
    {% include 'routing_config.j2' %}
exit
</code></pre>
<h2>3. Custom Filters</h2>
<pre><code class="language-python">def ip_increment(ip, increment=1):
    """Increment IP address"""
    parts = ip.split('.')
    parts[3] = str(int(parts[3]) + increment)
    return '.'.join(parts)

# Add to Jinja2 environment
env.filters['ip_increment'] = ip_increment
</code></pre>
<h2>4. Usage in template</h2>
<pre><code class="language-python">neighbor {{ base_ip|ip_increment(1) }}
neighbor {{ base_ip|ip_increment(2) }}
</code></pre>
<h2>5. Conditional Includes</h2>
<pre><code class="language-python">{% if device_type == 'nokia' %}
    {% include 'nokia_specific.j2' %}
{% elif device_type == 'cisco' %}
    {% include 'cisco_specific.j2' %}
{% endif %}
</code></pre>
<h1>Detailed Internal Jinja2 Logic Flow</h1>
<h2>How Templates Process Data</h2>
<pre><code class="language-python">        Device Inventory (devices.yaml)
                   |
                   v
        +----------------------+
        | Load Data in Python  |
        | (dict / variables)   |
        +----------+-----------+
                   |
                   v
        +----------------------+
        | Pass Data to Jinja2  |
        |  template.render()   |
        +----------+-----------+
                   |
        +----------+-----------+
        |                      |
        v                      v
+-------------------+   +-------------------+
|   Variables       |   |   Conditionals    |
| {{ hostname }}    |   | if vendor == SR OS|
| {{ interfaces }}  |   | vendor logic      |
+-------------------+   +-------------------+
        |                      |
        +----------+-----------+
                   |
                   v
        +----------------------+
        |        Loops         |
        | for interface in list|
        +----------+-----------+
                   |
                   v
        +----------------------+
        |       Filters        |
        | upper, default, join |
        +----------+-----------+
                   |
                   v
        +----------------------+
        | Final Config Output  |
        | Ready for Deployment |
        +----------------------+
</code></pre>
<h1>Jinja2 + YAML Workflow (Advanced Automation Process)</h1>
<pre><code class="language-python">   devices.yaml (YAML Inventory)
                    |
                    v
            Python Script (Loader)
                    |
                    v
            Jinja2 Template Engine
                    |
                    v
        Rendered Router Configuration
                    |
                    v
        Deployment (SSH / NETCONF / API)
</code></pre>
<h1>What’s Next?</h1>
<p>In Part 1, we examined the principles of YAML and Jinja2 and their application in dynamic configuration generation. This enables network engineers to transition from static, manual CLI configurations to automated, data-driven deployments. This methodology ensures consistency, minimizes human error, and expedites deployment in production networks.</p>
<p>In Part 2 , we are going to show practical examples of dynamic configuration generation. We will go through a few use cases :</p>
<p>Use Case 1 : Nokia SROS service Configuration</p>
<p>Use Case 2 : Nokia SROS Interface Configuration</p>
<p>Use Case 3: Multivendor BGP Configuration ( Nokia SROS, Juniper, Cisco)</p>
<h1>Questions ?</h1>
<p>Reach out on LinkedIn!</p>
]]></content:encoded></item><item><title><![CDATA[How to Use API Networking to Retrieve Juniper vMX Device Information]]></title><description><![CDATA[Welcome back to our Network Automation series. Last week, we utilized SSH and Netmiko to back up configurations. This week, we will explore modern network APIs. Rather than delving into complex setups]]></description><link>https://codednetwork.com/how-to-use-api-networking-to-retrieve-juniper-vmx-device-information</link><guid isPermaLink="true">https://codednetwork.com/how-to-use-api-networking-to-retrieve-juniper-vmx-device-information</guid><category><![CDATA[junos]]></category><category><![CDATA[Juniper]]></category><category><![CDATA[api]]></category><category><![CDATA[network api]]></category><category><![CDATA[automation]]></category><category><![CDATA[netconf]]></category><category><![CDATA[inventory]]></category><category><![CDATA[xml]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 17 Feb 2026 07:10:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770778358464/f6ed4842-91e6-496f-954b-7bbe3dea07db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome back to our Network Automation series. Last week, we utilized SSH and Netmiko to back up configurations. This week, we will explore <strong>modern network APIs</strong>. Rather than delving into complex setups, we will start with a fundamental task that every network engineer performs daily: <strong>gathering device information.</strong></p>
<p>By the conclusion of this post, you will be equipped to utilize network APIs to automatically gather:</p>
<ul>
<li><p>Device hostnames and software versions</p>
</li>
<li><p>Interface status</p>
</li>
<li><p>Device Uptime and Model</p>
</li>
</ul>
<p><strong>No configuration changes, no risk - just reading information the modern way.</strong></p>
<h1>Why Use APIs for Information Gathering?</h1>
<h2>The traditional method (SSH/CLI):</h2>
<pre><code class="language-bash">ssh admin@router1
show version | include Version
show interfaces terse
show system uptime
# Now manually copy-paste to Excel 
# Repeat for 50+ devices
</code></pre>
<h2>The API Approach:</h2>
<pre><code class="language-python"># Collect from 50+ devices in seconds
for device in devices:
  info =get_device_info(device)
# Automatically formatted in structured data
</code></pre>
<h2>Why is this a superior approach?</h2>
<ol>
<li><p><strong>Speed</strong>: Gather data from multiple devices simultaneously</p>
</li>
<li><p><strong>Structured Data</strong>: Utilize XML/JSON instead of parsing text</p>
</li>
<li><p><strong>Consistency</strong>: Ensure the same data format every time</p>
</li>
<li><p><strong>No Manual Copying</strong>: Directly export to Excel, database, or dashboard</p>
</li>
<li><p><strong>Automation Ready</strong>: Simple to schedule and monitor</p>
</li>
<li><p><strong>Error Reduction</strong>: Eliminate copy-paste mistakes</p>
</li>
</ol>
<h1>Understanding Network APIs</h1>
<h2>What is an API?</h2>
<p>An <strong>API (Application Programming Interface)</strong> serves as a means for programs to communicate with network devices. It is comparable to a menu at a restaurant:</p>
<ul>
<li><p>The menu displays available options <strong>(API documentation)</strong></p>
</li>
<li><p>You place an order <strong>(API call)</strong></p>
</li>
<li><p>You receive the requested item <strong>(API response)</strong></p>
</li>
</ul>
<h2>Two Primary Types for Network Devices:</h2>
<h3>NETCONF</h3>
<ul>
<li><p>Uses SSH connection (port 830)</p>
</li>
<li><p>Structured data (XML)</p>
</li>
<li><p>Supports transactions and validation</p>
</li>
<li><p>More powerful, model-driven</p>
</li>
</ul>
<details>
<summary>Note:</summary>
<p><em><mark class="bg-yellow-200 dark:bg-yellow-500/30">In this article we touch on NETCONF briefly just for common understanding. NETCONF will have a dedicated article with more in-depth details and more practical examples.</mark></em></p>
</details>

<h3>NETCONF Session Flow</h3>
<pre><code class="language-plaintext">Client                                  Network Device (Server)
|                                               |
| SSH Connection (port 830) |
| ==========================================&gt;   |
|                                               |
| &lt;hello&gt; (device capabilities) |
| &lt;-------------------------------------------  |
| &lt;hello&gt; (client capabilities) |
| -------------------------------------------&gt;  |
|                                               |
| &lt;rpc message-id="101"&gt;                        |
| &lt;lock&gt;                                        |
| &lt;target&gt;                                      |
| &lt;candidate/&gt;                                  |
| &lt;/target&gt;                                     |
| &lt;/lock&gt;                                       |
| &lt;/rpc&gt;                                        |
| -------------------------------------------&gt;  |
|                                               |
| &lt;rpc-reply message-id="101"&gt;                  |
| &lt;ok/&gt;                                         |
| &lt;/rpc-reply&gt;                                  |
| &lt;-------------------------------------------  |
|                                               |
</code></pre>
<h3>REST (Representational State Transfer) API</h3>
<ul>
<li><p>Uses HTTP/HTTPS</p>
</li>
<li><p>Usually JSON format</p>
</li>
<li><p>Simple request/response</p>
</li>
<li><p>Easy to understand</p>
</li>
</ul>
<h3>How REST Works</h3>
<pre><code class="language-plaintext">Client                            Server (Network Device)
|                                          |
| GET /api/v1/interfaces/eth0              |
| ----------------------------------------&gt;|
|                                          |
| HTTP/1.1 200 OK                          |
| Content-Type: application/json           |
| {                                        |
| "name": "eth0",                          |
| "status": "up",                          |
| "speed": "1000Mbps"                      |
| }                                        |
| &lt;----------------------------------------|
|                                          |
</code></pre>
<h3>REST Operations</h3>
<pre><code class="language-plaintext">HTTP Method   Purpose          Example
-----------   -------          -------
GET           Read resource    GET /api/interfaces
POST          Create resource  POST /api/interfaces
PUT           Update resource  PUT /api/interfaces/eth0
DELETE        Remove resource  DELETE /api/interfaces/eth1
PATCH         Partial update   PATCH /api/interfaces/eth0
</code></pre>
<h2>When to Use Each Method</h2>
<h3>Select REST when:</h3>
<ul>
<li><p>Utilize REST for straightforward and rapid integrations with network devices.</p>
</li>
<li><p>It is ideal for modern cloud-native applications.</p>
</li>
<li><p>Select REST if the vendor offers a well-documented REST API.</p>
</li>
<li><p>Opt for REST when seeking extensive support for various languages and tools.</p>
</li>
</ul>
<h3>Select NETCONF when:</h3>
<ul>
<li><p>You need transactional integrity for configuration changes</p>
</li>
<li><p>Working with critical infrastructure requiring validation</p>
</li>
<li><p>Managing complex, multi-step configurations</p>
</li>
<li><p>Rollback capabilities are essential Working with devices that support YANG data models</p>
</li>
<li><p>Enterprise network automation at scale</p>
</li>
</ul>
<h1>Prerequisites</h1>
<h2>Software Requirements</h2>
<pre><code class="language-python"># Install required Python libraries
pip install ncclient  # For NETCONF
pip install xmltodict # For parsing XML
pip install tabulate  # For pretty tables
pip install requests  # For REST APIs (if needed)
</code></pre>
<h2>Lab Setup</h2>
<p>For this tutorial, you will need access to:</p>
<ul>
<li>A Juniper vMX router with NETCONF enabled</li>
</ul>
<p><strong>Don't have a device?</strong> Use <a href="https://containerlab.dev/">containerlab</a></p>
<h2>Enable NETCONF on your device:</h2>
<h3>Juniper vMX</h3>
<pre><code class="language-python">set system services netconf ssh
set system services netconf rfc-compliant
commit
</code></pre>
<h3>Verify NETCONF Configuration and Status</h3>
<pre><code class="language-python">admin@juniper&gt; show configuration system services netconf    
ssh;
rfc-compliant;

admin@juniper&gt; show system connections | match 830           
tcp6       0      0  *.830        *.*                                           LISTEN
tcp4       0      0  *.830        *.*                                           LISTEN
</code></pre>
<h1>Juniper vMX - System Information Accessed via NETCONF</h1>
<p>In this example, we are gathering basic information from a Juniper vMX router.</p>
<h2>Understanding the Code Structure</h2>
<pre><code class="language-python">from ncclient import manager
import xmltodict
from tabulate import tabulate
import getpass
import csv
from datetime import datetime
import os
</code></pre>
<h3>Explanation of Each Import</h3>
<ul>
<li><p><code>from ncclient import manager</code>: This import allows us to manage NETCONF sessions with network devices.</p>
</li>
<li><p><code>import xmltodict</code>: This library is used to convert XML data into a Python dictionary, making it easier to work with.</p>
</li>
<li><p><code>from tabulate import tabulate</code>: This module helps in creating well-formatted tables for displaying data.</p>
</li>
<li><p><code>import getpass</code>: This module is used to securely prompt for a password without displaying it on the screen.</p>
</li>
<li><p><code>import csv</code>: This library provides functionality to read from and write to CSV files.</p>
</li>
<li><p><code>from datetime import datetime</code>: This import allows us to work with dates and times in our code.</p>
</li>
<li><p><code>import os</code>: This module provides a way to interact with the operating system, such as handling file paths.</p>
</li>
</ul>
<h3>Device Connection Details</h3>
<pre><code class="language-python"># Device details
VMX_HOST = "172.20.20.16"
VMX_PORT = 830
VMX_USER = "admin"
VMX_PASS = getpass.getpass(f"Password for {VMX_USER}@{VMX_HOST}: ")
</code></pre>
<h3>Why use port 830?</h3>
<ul>
<li><p>Port 830 is the standard port for NETCONF over SSH.</p>
</li>
<li><p>Port 22 is used for regular SSH connections.</p>
</li>
<li><p>Port 830 provides us with NETCONF capabilities.</p>
</li>
</ul>
<h3>Why use <code>getpass</code>?</h3>
<pre><code class="language-python"># Bad - password visible in code and on screen
VMX_PASS = "coded123"
# Good - password not shown when typing
VMX_PASS = getpass.getpass("Password: ")
</code></pre>
<h3>The Connection Function</h3>
<pre><code class="language-python">def connect_vmx():
    """Connect to Juniper vMX via NETCONF"""
    return manager.connect(
        host=VMX_HOST,
        port=VMX_PORT,
        username=VMX_USER,
        password=VMX_PASS,
        device_params={'name': 'junos'},
        hostkey_verify=False,
        look_for_keys=False,
        allow_agent=False
    )
</code></pre>
<h3>Breaking Down the Parameters:</h3>
<ul>
<li><p><code>host=VMX_HOST</code> = The IP address for connection</p>
</li>
<li><p><code>port=VMX_PORT</code> = Port 830 (used for NETCONF)</p>
</li>
<li><p><code>username=VMX_USER</code> = The login username</p>
</li>
<li><p><code>password=VMX_PASS</code> = The login password</p>
</li>
<li><p><code>device_params={'name': 'junos'}</code> = Informs ncclient that this is a Juniper device</p>
</li>
<li><p><code>hostkey_verify=False</code> = Disables SSH key verification (suitable for lab environments, not recommended for production)</p>
</li>
<li><p><code>look_for_keys=False</code> = Disables searching for SSH key files</p>
</li>
<li><p><code>allow_agent=False</code> = Disables the use of the SSH agent</p>
</li>
</ul>
<h3>What <code>manager.connect()</code> returns:</h3>
<ul>
<li><p>A connection object that allows us to send commands</p>
</li>
<li><p>Automatically manages the SSH connection</p>
</li>
<li><p>Oversees the NETCONF session</p>
</li>
</ul>
<h3>Retrieving System Information</h3>
<pre><code class="language-python">def get_vmx_system_info():
    """Get system information from vMX"""
    
    with connect_vmx() as m:
        # Get system information
        result = m.command(command='show version', format='xml')
        data = xmltodict.parse(result.tostring)
        
        # Get uptime
        uptime_result = m.command(command='show system uptime', format='xml')
        uptime_data = xmltodict.parse(uptime_result.tostring)
        
        return data, uptime_data
</code></pre>
<h3>What is <code>with ... as m</code>?</h3>
<pre><code class="language-python">with connect_vmx() as m:
# Use connection 'm' here
# Connection automatically closes when done
</code></pre>
<p>This is a <strong>context manager</strong></p>
<p>Benefits:</p>
<ul>
<li><p>Automatically closes connection even if there's an error</p>
</li>
<li><p>Prevents connection leaks</p>
</li>
<li><p>Cleaner than manual connect/disconnect</p>
</li>
</ul>
<h3>The <code>m.command()</code> method:</h3>
<pre><code class="language-python">result = m.command(command='show version', format='xml')
</code></pre>
<h3>Breaking it down:</h3>
<ul>
<li><p><code>m.command()</code> = Executes a Junos operational command</p>
</li>
<li><p><code>command='show version'</code> = The specific CLI command to be executed</p>
</li>
<li><p><code>format='xml'</code> = Retrieves the response in XML format (structured data)</p>
</li>
</ul>
<h3>Why use XML instead of text?</h3>
<p>Text output:</p>
<pre><code class="language-plaintext">Hostname: juniper
Model: vmx
Junos: 22.4R1.10
</code></pre>
<p>XML output:</p>
<pre><code class="language-xml">&lt;software-information&gt;
  &lt;host-name&gt;juniper&lt;/host-name&gt;
  &lt;product-model&gt;vmx&lt;/product-model&gt;
  &lt;junos-version&gt;22.4R1.10&lt;/junos-version&gt;
&lt;/software-information&gt;
</code></pre>
<p>XML is:</p>
<ul>
<li><p>Structured (easy to parse)</p>
</li>
<li><p>Consistent format</p>
</li>
<li><p>Machine-readable</p>
</li>
<li><p>Contains all information</p>
</li>
</ul>
<h3>Converting XML to Dictionary:</h3>
<pre><code class="language-python">uptime_data = xmltodict.parse(uptime_result.tostring)
</code></pre>
<p>This process converts XML data into a Python dictionary:</p>
<pre><code class="language-python">data ={
   'rpc-reply':{
      'software-information':{
        'host-name':'juniper',
        'product-model':'vmx',
        'junos-version':'22.4R1.110'
       }
    }
}
</code></pre>
<h3>Extracting and formatting the data</h3>
<pre><code class="language-python">def parse_system_data(sys_data, uptime_data):
    """Parse and extract system information"""
    
    try:
        software = sys_data['rpc-reply']['software-information']
        uptime_info = uptime_data['rpc-reply']['system-uptime-information']
        
        info = {
            'hostname': software.get('host-name', 'N/A'),
            'model': software.get('product-model', 'N/A'),
            'version': software.get('junos-version', 'N/A'),
            'uptime': uptime_info.get('system-booted-time', {}).get('time-length', 'N/A')
        }
        
        return info
        
    except Exception as e:
        print(f"Could not parse system info: {e}")
        return {
            'hostname': 'N/A',
            'model': 'N/A',
            'version': 'N/A',
            'uptime': 'N/A'
        }
</code></pre>
<h3>Why <code>try/except</code> ?</h3>
<ul>
<li><p>The XML structure may vary</p>
</li>
<li><p>Missing fields will not cause the script to crash</p>
</li>
<li><p>Ensures graceful error handling</p>
</li>
</ul>
<h3>What is <code>.get('key', 'N/A')</code> ?</h3>
<p>Safe dictionary access:</p>
<pre><code class="language-python"># If key exists, return value
# If key doesn't exist, return 'N/A' instead of crashing
value=dictionary.get('key','N/A')
</code></pre>
<h3>Displaying the information</h3>
<pre><code class="language-python">def display_system_info(system_info):
    """Display system information in formatted output"""
    
    print("="*60)
    print("Juniper vMX Device Information")
    print("="*60)
</code></pre>
<h3>Creating the headers</h3>
<pre><code class="language-python">print("="*60)
# Prints 60 equal signs
</code></pre>
<h3>Exporting to CSV</h3>
<p>Let's save the collected data to a CSV file for use in Excel:</p>
<pre><code class="language-python">def export_system_to_csv(system_info, filename=None):
    """Export system information to CSV file"""
    
    if filename is None:
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        filename = f"vmx_system_info_{timestamp}.csv"
    
    # Create exports directory if it doesn't exist
    export_dir = 'exports'
    if not os.path.exists(export_dir):
        os.makedirs(export_dir)
    
    filepath = os.path.join(export_dir, filename)
    
    with open(filepath, 'w', newline='') as f:
        writer = csv.writer(f)
        
        # Write header
        writer.writerow([
            'Collection Time',
            'Device IP',
            'Hostname',
            'Model',
            'Junos Version',
            'Uptime'
        ])
        
        # Write data
        writer.writerow([
            datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            VMX_HOST,
            system_info['hostname'],
            system_info['model'],
            system_info['version'],
            system_info['uptime']
        ])
    
    print(f"\n✓ System information exported to: {filepath}")
    return filepath
</code></pre>
<h2>Complete Output</h2>
<pre><code class="language-python"># Exexute the script 
python3 junos_get_csv.py

============================================================
vMX Device Information Collector
============================================================
Target: 172.20.20.16
Time: 2026-02-11 13:14:13
============================================================

Collecting system information...
✓ System information collected
Collecting interface information...
✓ Interface information collected (10 interfaces)

============================================================
Juniper vMX Device Information
============================================================
Hostname            : juniper
Model               : vmx
Junos Version       : 22.4R1.10
Uptime              : {'@seconds': '2431474', '#text': '4w0d 03:24'}

============================================================
Interface Status
============================================================
+-------------+----------------+---------------+
| Interface   | Admin Status   | Oper Status   |
+=============+================+===============+
| ge-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| gr-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| ip-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| lc-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| lt-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| mt-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| pd-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| pe-0/0/0    | up             | up            |
+-------------+----------------+---------------+
| pfe-0/0/0   | up             | up            |
+-------------+----------------+---------------+
| pfh-0/0/0   | up             | up            |
+-------------+----------------+---------------+

============================================================
Exporting Data
============================================================

✓ System information exported to: exports/vmx_system_info_20260211_131415.csv
✓ Interface information exported to: exports/vmx_interfaces_20260211_131415.csv

============================================================
Collection Summary
============================================================
✓ Device: juniper (172.20.20.16)
✓ Model: vmx
✓ Version: 22.4R1.10
✓ Interfaces Collected: 10
✓ System CSV: exports/vmx_system_info_20260211_131415.csv
✓ Interface CSV: exports/vmx_interfaces_20260211_131415.csv
============================================================
</code></pre>
<h1>Troubleshooting Common Issues</h1>
<h2>Issue 1: Connection Timeout</h2>
<h3>Error:</h3>
<pre><code class="language-python">TimeoutError: timed out
</code></pre>
<h3>Solutions:</h3>
<pre><code class="language-python"># Increase timeout
m = manager.connect(
  host=device['host'],
  timeout=30, # Increase from default 10 seconds
  ...
)
</code></pre>
<h2>Issue 2: Authentication Failed</h2>
<h3>Error:</h3>
<pre><code class="language-python">AuthenticationException: Authentication failed
</code></pre>
<h3>Solutions:</h3>
<ol>
<li><p>Verify username/password</p>
</li>
<li><p>Check user has correct privileges</p>
</li>
<li><p>Ensure NETCONF is enabled</p>
</li>
</ol>
<h2>Issue 3: XML Parsing Errors</h2>
<h3>Error:</h3>
<pre><code class="language-python">KeyError: 'software-information'
</code></pre>
<h3>Solutions:</h3>
<pre><code class="language-python"># Use .get() with defaults
software=data.get('rpc-reply',{}).get('software-information',{})
hostname=software.get('host-name','Unknown')
</code></pre>
<h2>Issue 4: NETCONF Not Enabled</h2>
<h3>Error:</h3>
<pre><code class="language-python">Connection refused on port 830
</code></pre>
<h3>Solutions:</h3>
<pre><code class="language-python">show system connections | match 830 
</code></pre>
<h1>Key Differences: NETCONF vs. SSH/CLI</h1>
<table style="min-width:75px"><colgroup><col style="min-width:25px"></col><col style="min-width:25px"></col><col style="min-width:25px"></col></colgroup><tbody><tr><th><p><strong>Feature</strong></p></th><th><p><strong>NETCONF</strong></p></th><th><p><strong>CLI</strong></p></th></tr><tr><td><p><strong>Data Format</strong></p></td><td><p>XML (structured)</p></td><td><p>Text (unstructured)</p></td></tr><tr><td><p><strong>Parsing</strong></p></td><td><p>Easy (XML parsing)</p></td><td><p>Hard ( Regex , text parsing)</p></td></tr><tr><td><p><strong>Consistency</strong></p></td><td><p>Always same format</p></td><td><p>Can change between versions</p></td></tr><tr><td><p><strong>Speed</strong></p></td><td><p>Fast</p></td><td><p>Slower</p></td></tr><tr><td><p><strong>Validation</strong></p></td><td><p>Built in</p></td><td><p>None</p></td></tr><tr><td><p><strong>Transactional Configurations</strong></p></td><td><p>Supported</p></td><td><p>Not supported</p></td></tr></tbody></table>

<h1>Next Steps</h1>
<p>Now that you can collect device information, consider the following actions:</p>
<ol>
<li><p><strong>Schedule Regular Collection</strong> - Utilize cron or Task Scheduler</p>
</li>
<li><p><strong>Develop a Dashboard</strong> - Present data in real-time</p>
</li>
<li><p><strong>Track Changes</strong> - Compare today's data with yesterday's data.</p>
</li>
<li><p><strong>Alert on Differences</strong> - Send an email notification when versions change.</p>
</li>
<li><p><strong>Export to Database</strong> - Store historical data</p>
</li>
</ol>
<h1>Key Takeaways</h1>
<p>✅ <strong>NETCONF is powerful</strong> - It offers structured data, supports transactions, and provides validation.</p>
<p>✅ <strong>XML is consistent</strong> - It maintains the same format consistently.</p>
<p>✅ <strong>Python simplifies the process</strong> - ncclient manages the complexity</p>
<p>✅ <strong>Start with read-only</strong> - This approach eliminates the risk of causing disruptions.</p>
<p>✅ <strong>Build confidence</strong> - Gain expertise in querying before proceeding to configuration.</p>
<h1>Conclusion</h1>
<p>Congratulations on mastering the use of NETCONF to gather device information from Juniper vMX routers :</p>
<ul>
<li><p>Establish a connection to the device using NETCONF</p>
</li>
<li><p>Execute operational commands</p>
</li>
<li><p>Parse XML responses</p>
</li>
<li><p>Extract and format the data</p>
</li>
<li><p>Export data to CSV for analysis</p>
</li>
</ul>
<p>Next week, we will utilize Jinja2 templates to dynamically generate configurations across multiple vendors.</p>
<h1>Download the code</h1>
<p>All the code from this post is available on GitLab:<a href="https://gitlab.com/kgosileburu/network-automation-scripts">network-automation-week-2</a></p>
<h1><strong>Questions or Feedback?</strong></h1>
<p>Connect with me on LinkedIn</p>
]]></content:encoded></item><item><title><![CDATA[Introduction to Network Automation: Build Your First Backup Script with Python for a multi-vendor environment - Part 2]]></title><description><![CDATA[Welcome back to Part 2 of our network automation series. In Part 1, we explored the fundamentals of Netmiko, including connecting to a device and executing commands. Now, we will apply those skills.
I]]></description><link>https://codednetwork.com/introduction-to-network-automation-build-your-first-backup-script-with-python-for-a-multi-vendor-environment-part-2</link><guid isPermaLink="true">https://codednetwork.com/introduction-to-network-automation-build-your-first-backup-script-with-python-for-a-multi-vendor-environment-part-2</guid><category><![CDATA[Juniper]]></category><category><![CDATA[Cisco]]></category><category><![CDATA[nokia]]></category><category><![CDATA[Python]]></category><category><![CDATA[Network Automation]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 10 Feb 2026 08:23:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768966872820/88573762-8193-41be-8c15-6b40ae5392c0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Welcome back to Part 2 of our network automation series.</strong> In Part 1, we explored the fundamentals of Netmiko, including connecting to a device and executing commands. Now, we will apply those skills.</p>
<p>In this post, we will develop a practical configuration backup script to automate a common task for network engineers: backing up device configurations across multiple network devices. Instead of manually SSH-ing into each router or switch, you will have a Python script that performs this task automatically and saves timestamped backups.</p>
<p><strong>What you'll learn:</strong></p>
<ul>
<li><p>Efficiently managing multiple device connections</p>
</li>
<li><p>Implementing error handling for practical scenarios</p>
</li>
<li><p>Organizing and timestamping backup files</p>
</li>
</ul>
<p><strong>What you'll need:</strong></p>
<ul>
<li><p>A basic understanding of Netmiko (as covered in Part 1)</p>
</li>
<li><p>Python 3.x installed</p>
</li>
<li><p>Access to network devices (whether physical, virtual, or via Containerlab)</p>
</li>
<li><p>A text editor or IDE</p>
</li>
</ul>
<p>By the end of this tutorial, you'll have a working automation script that you can adapt for your own network environment. Let's get started</p>
<h1>Building Your First Backup Script</h1>
<h2>Version 2: Multi-Vendor Support</h2>
<p>Now, let's extend this to support Nokia, Cisco, and Juniper devices.</p>
<pre><code class="language-python">from netmiko import ConnectHandler
from datetime import datetime
import os
import sys

# Device inventory - add your devices here
devices = [
    {
        'device_type': 'alcatel_sros',
        'host': '172.20.20.13',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'nokia'
    },
    {
        'device_type': 'cisco_xr',  # Use 'cisco_xe' for IOS-XE, 'cisco_xr' for IOS-XR
        'host': '172.20.20.15',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'cisco'
    },
    {
        'device_type': 'juniper_junos',
        'host': '172.20.20.16',
        'username': 'username',
        'password': 'password',
        'timeout': 60,
        'vendor': 'juniper'
    }
]

# Vendor-specific commands
VENDOR_COMMANDS = {
    'nokia': {
        'pager': 'environment no more',
        'config': 'admin display-config',
        'prompt': r'#'
    },
    'cisco': {
        'pager': 'terminal length 0',
        'config': 'show running-config',
        'prompt': r'#'
    },
    'juniper': {
        'pager': 'set cli screen-length 0',
        'config': 'show configuration',
        'prompt': r'[&gt;#]'
    }
}

# Create backup directory if it doesn't exist
backup_dir = 'backups'
if not os.path.exists(backup_dir):
    os.makedirs(backup_dir)
    print(f"Created backup directory: {backup_dir}\n")

def validate_config(config_text, vendor):
    """Validate that configuration was retrieved successfully"""
    if not config_text or len(config_text) &lt; 100:
        return False, "Configuration appears empty or too short"

    # Vendor-specific validation
    validation_keywords = {
        'nokia': ['configure', 'system'],
        'cisco': ['version', 'interface'],
        'juniper': ['system', 'interfaces']
    }

    keywords = validation_keywords.get(vendor, [])
    config_lower = config_text.lower()

    if not any(keyword in config_lower for keyword in keywords):
        return False, f"Configuration doesn't appear to be valid {vendor.upper()} config"

    return True, "Configuration validated"

def backup_device(device_info):
    """Backup a single device and return status"""
    vendor = device_info.get('vendor', 'unknown')
    host = device_info['host']

    print(f"\n{'='*70}")
    print(f"Processing {vendor.upper()} device: {host}")
    print(f"{'='*70}")

    try:
        # Get vendor-specific commands
        commands = VENDOR_COMMANDS.get(vendor)
        if not commands:
            return False, f"Unknown vendor: {vendor}"

        # Create connection parameters (exclude 'vendor' key)
        connection_params = {k: v for k, v in device_info.items() if k != 'vendor'}

        # Connect to device
        print(f"Connecting to {host}...")
#        connection = ConnectHandler(**device_info)
        connection = ConnectHandler(**connection_params)
        print("✓ Connected successfully")

        # Get hostname for filename
#        hostname = connection.find_prompt().strip('#&gt; ')
#        print(f"✓ Device hostname: {hostname}")

        # Get hostname for filename
        raw_hostname = connection.find_prompt().strip('#&gt; ')

        # Clean hostname - remove invalid characters for filenames
        # For Cisco XR: RP/0/RP0/CPU0:hostname becomes hostname
        if ':' in raw_hostname:
            hostname = raw_hostname.split(':')[-1]  # Get part after last colon
        else:
            hostname = raw_hostname

        # Remove any remaining invalid filename characters
        hostname = hostname.replace('/', '_').replace('\\', '_').replace(':', '_')


        # Set terminal parameters
        print("Setting terminal parameters...")
        connection.send_command(commands['pager'], expect_string=commands['prompt'])

        # Retrieve configuration
        print(f"Retrieving configuration from {hostname}...")
        config = connection.send_command(
            commands['config'],
            expect_string=commands['prompt'],
            delay_factor=2
        )

        # Validate configuration
        is_valid, message = validate_config(config, vendor)
        print(f"✓ {message}")

        if not is_valid:
            print(f"⚠ Warning: {message}")
            connection.disconnect()
            return False, message

        # Create timestamp for filename
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

        # Create vendor subdirectory
        vendor_dir = os.path.join(backup_dir, vendor)
        if not os.path.exists(vendor_dir):
            os.makedirs(vendor_dir)

        # Create filename
        filename = f"{vendor_dir}/{hostname}_{timestamp}.txt"

        # Save configuration to file
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(f"# Backup created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write(f"# Device: {hostname} ({host})\n")
            f.write(f"# Vendor: {vendor.upper()}\n")
            f.write(f"# Device Type: {device_info['device_type']}\n")
            f.write(f"# {'='*60}\n\n")
            f.write(config)

        # Verify file was created and has content
        file_size = os.path.getsize(filename)
        print(f"✓ Configuration saved to {filename}")
        print(f"✓ File size: {file_size:,} bytes")

        # Disconnect
        connection.disconnect()
        print("✓ Disconnected successfully")

        return True, filename

    except ConnectionError as e:
        error_msg = f"Connection error: {str(e)}"
        print(f"✗ {error_msg}")
        return False, error_msg

    except Exception as e:
        error_msg = f"Error: {str(e)} (Type: {type(e).__name__})"
        print(f"✗ {error_msg}")
        return False, error_msg

# Main execution
def main():
    """Backup all devices in inventory"""
    print("\n" + "="*70)
    print("MULTI-VENDOR NETWORK DEVICE BACKUP")
    print("="*70)
    print(f"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Total devices: {len(devices)}\n")

    results = {
        'success': [],
        'failed': []
    }

    # Process each device
    for device in devices:
        success, info = backup_device(device)

        if success:
            results['success'].append({
                'host': device['host'],
                'vendor': device.get('vendor', 'unknown'),
                'file': info
            })
        else:
            results['failed'].append({
                'host': device['host'],
                'vendor': device.get('vendor', 'unknown'),
                'error': info
            })

    # Print summary
    print("\n" + "="*70)
    print("BACKUP SUMMARY")
    print("="*70)
    print(f"Successful: {len(results['success'])}/{len(devices)}")
    print(f"Failed: {len(results['failed'])}/{len(devices)}")

    if results['success']:
        print("\n✓ Successful backups:")
        for item in results['success']:
            print(f"  - {item['vendor'].upper()}: {item['host']}")

    if results['failed']:
        print("\n✗ Failed backups:")
        for item in results['failed']:
            print(f"  - {item['vendor'].upper()}: {item['host']}")
            print(f"    Reason: {item['error']}")

    print(f"\nEnd time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("="*70 + "\n")

    # Exit with appropriate code
    sys.exit(0 if not results['failed'] else 1)

if __name__ == "__main__":
    main()
</code></pre>
<h2>Expected Output</h2>
<pre><code class="language-python">python3 multivendorbackup.py

======================================================================
MULTI-VENDOR NETWORK DEVICE BACKUP
======================================================================
Start time: 2026-01-22 21:14:29
Total devices: 3


======================================================================
Processing NOKIA device: 172.20.20.13
======================================================================
Connecting to 172.20.20.13...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from PE1...
✓ Configuration validated
✓ Configuration saved to backups/nokia/PE1_20260122_211432.txt
✓ File size: 4,263 bytes
✓ Disconnected successfully

======================================================================
Processing CISCO device: 172.20.20.15
======================================================================
Connecting to 172.20.20.15...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from cisco_xrv9000...
✓ Configuration validated
✓ Configuration saved to backups/cisco/cisco_xrv9000_20260122_211434.txt
✓ File size: 2,529 bytes
✓ Disconnected successfully

======================================================================
Processing JUNIPER device: 172.20.20.16
======================================================================
Connecting to 172.20.20.16...
✓ Connected successfully
Setting terminal parameters...
Retrieving configuration from admin@juniper...
✓ Configuration validated
✓ Configuration saved to backups/juniper/admin@juniper_20260122_211435.txt
✓ File size: 1,114 bytes
✓ Disconnected successfully

======================================================================
BACKUP SUMMARY
======================================================================
Successful: 3/3
Failed: 0/3

✓ Successful backups:
  - NOKIA: 172.20.20.13
  - CISCO: 172.20.20.15
  - JUNIPER: 172.20.20.16

End time: 2026-01-22 21:14:35
======================================================================
</code></pre>
<h2>Lets analyze the script for a clearer understanding</h2>
<ol>
<li><p><code>from netmiko import ConnectHandler</code> Firstly we import the <code>ConnectHandler</code> from the <code>netmiko</code> library used to connect into network devices</p>
</li>
<li><p><code>from datetime import datetime</code> We then import <code>datetime</code> from the <code>datetime</code> library , to be able to work with time and dates</p>
</li>
<li><p><code>import os</code> which is an operating system library used for working with files and folders</p>
</li>
<li><p><code>import sys</code> used for system-level operations</p>
</li>
<li><p><strong>Device Inventory</strong>: This is where we define all the devices we want to back up. The <code>devices</code> variable holds our list of all devices. Inside the list, each device is represented by a dictionary</p>
</li>
<li><p><code>vendor_commands</code> A dictionary of dictionaries (nested dictionaries). Each vendor has its own set of commands</p>
</li>
<li><p>Create a backup directory if it doesn’t exist. <code>backup_dir = 'backups'</code> a variable is created named <code>backup_dir</code> . This is the name of the folder where we will store the backups. <code>os.path.exists(backup_dir)</code></p>
<p>Checks if the folder exists and it returns <code>True</code> if it exists, <code>False</code> if it doesn't</p>
</li>
<li><p>The <strong>Validation Function</strong> <code>def validate_config(config_text, vendor)</code> checks if <code>config_text</code> is empty or None and returns True if it is empty, and False if it has content. The <code>or</code> operator means if either condition is true, the entire expression is true. <code>len(config_text) &lt; 100</code> Checks if config has fewer than 100 characters. <code>return</code> returns two values to the function <code>false</code> = validation failed and <code>"Configuration appears..."</code> = Error Message .</p>
</li>
<li><p><strong>Vendor-Specific Validation</strong> involves creating a dictionary of keywords we expect to find. Each vendor's configuration should include certain words. If these words are missing, there could be a problem..</p>
</li>
<li><p><code>def backup_device(device_info)</code>: This function takes one input, <code>device_info</code>, which is a dictionary containing the device details. <code>vendor = device_info.get('vendor', 'unknown')</code> uses <code>.get()</code> to safely retrieve the vendor. If the 'vendor' key doesn't exist, it uses 'unknown' as the default. <code>host = device_info['host']</code> gets the IP address from <code>device_info</code>.</p>
</li>
<li><p>Create connection parameters using <code>connection_params = {k: v for k, v in device_info.items() if k != 'vendor'}</code>. The <code>device_info.items()</code> retrieves pairs of (key, value), and <code>for k, v in ...</code> loops through each pair. If <code>k != 'vendor'</code>, it doesn't include the vendor, and <code>{k: v ...}</code> creates a new dictionary. Connect to the device.</p>
</li>
<li><p>Retrieve the <strong>hostname</strong> for the <strong>filename</strong> and sanitize it by removing any characters that are not valid for filenames.</p>
</li>
<li><p><code>connection.send_command(commands['pager'], expect_string=commands['prompt'])</code> used to turn off pagination</p>
</li>
<li><p>The <code>commands['config']</code> is used to retrieve the configuration, and <code>expect_string=commands['prompt']</code> specifies the prompt to wait for before continuing.</p>
</li>
<li><p><code>is_valid, message = validate_config(config, vendor)</code> calls the validation function. If the validation fails, it prints a warning.</p>
</li>
<li><p>Create a timestamp and a vendor directory. Use <code>os.path.join()</code> to combine folder paths.</p>
</li>
<li><p>Create the filename using <code>filename = f"{vendor_dir}/{hostname}_{timestamp}.txt"</code> and save the configuration.</p>
</li>
<li><p>Verify file creation by checking the file size in bytes with <code>os.path.getsize(filename)</code>.</p>
</li>
<li><p>Error Handling except ConnectionError as e : Catches only connection-related errors and except Exception as e : Catches any other error</p>
</li>
<li><p>The main function <code>main()</code> orchestrates the entire backup process. It calls <code>backup_device()</code> for each device and collects and displays the results.</p>
</li>
</ol>
<h2>How It All Works Together</h2>
<h3>The Complete Flow</h3>
<pre><code class="language-plaintext">1. Script starts
├─&gt; Import libraries
├─&gt; Define device list
├─&gt; Define vendor commands
└─&gt; Create backup directory

2. main() function runs
├─&gt; Print header
├─&gt; Create results dictionary
└─&gt; For each device:
├─&gt; Call backup_device()
│ ├─&gt; Connect to device
│ ├─&gt; Get hostname
│ ├─&gt; Disable paging
│ ├─&gt; Get configuration
│ ├─&gt; Validate configuration
│ ├─&gt; Save to file
│ ├─&gt; Disconnect
│ └─&gt; Return success/failure
│
└─&gt; Add to results (success or failed list)

3. Print summary
├─&gt; Show success count
├─&gt; Show failure count
├─&gt; List successful backups
├─&gt; List failed backups
└─&gt; Exit with status code
</code></pre>
<h1><strong>Best Practices and Tips</strong></h1>
<h2>Security Considerations</h2>
<p><strong>Avoid hardcoding passwords in scripts</strong>. Consider using one of the following approaches:</p>
<ul>
<li><p>Environmental variables</p>
</li>
<li><p>Encrypted vaults (Ansible Vault, HashiCorp Vault)</p>
</li>
<li><p>Keyring libraries</p>
</li>
<li><p>Prompts for passwords at runtime</p>
</li>
</ul>
<p>Example with environment variables:</p>
<pre><code class="language-python">devices = 
    {
        'device_type': 'alcatel_sros',
        'host': '172.20.20.13',
        'username': os.environ.get('NETWORK_USERNAME'),
        'password': os.environ.get('NETWORK_PASSWORD'),
  
    }
</code></pre>
<h2>Error Handling</h2>
<p>Always implement proper error handling:</p>
<ul>
<li><p>Connection timeouts</p>
</li>
<li><p>Authentication failures</p>
</li>
<li><p>Device unreachable</p>
</li>
<li><p>Insufficient privileges</p>
</li>
</ul>
<h2>Logging</h2>
<p>Comprehensive logging helps troubleshoot issues:</p>
<ul>
<li><p>Connection attempts</p>
</li>
<li><p>Successful operations</p>
</li>
<li><p>Errors with full stack traces</p>
</li>
<li><p>Execution duration</p>
</li>
</ul>
<h2>Backup Retention</h2>
<p>Implement a retention policy to manage disk space</p>
<h1>Troubleshooting Common Issues</h1>
<h2>Issue 1: "Authentication failed"</h2>
<p>Solution : Verify credentials, check if AAA is configured correctly</p>
<h2>Issue 2: "Connection timeout"</h2>
<p>Solution : Verify network connectivity, check firewall rules, ensure SSH is enabled</p>
<h2>Issue 3: "Command not recognized"</h2>
<p>Solution : Verify the backup command for your device type, some platforms use different commands</p>
<h2>Issue 4: "Permission denied"</h2>
<p>Solution : Ensure the user has proper privilege levels (Cisco: privilege 15, Juniper: superuser class)</p>
<h1>Next Steps</h1>
<p>Now that you have a functional backup script, consider implementing the following enhancements:</p>
<ol>
<li><p><strong>Schedule automated backups</strong> using cron (Linux) or Task Scheduler (Windows)</p>
</li>
<li><p><strong>Add Git integration</strong> to track configuration changes over time</p>
</li>
<li><p><strong>Implement differential backups</strong> to store only changes</p>
</li>
<li><p><strong>Add email notifications</strong> for backup failures</p>
</li>
<li><p><strong>Extend to more device types</strong> (Palo Alto, Fortinet etc.)</p>
</li>
</ol>
<h1>Conclusion</h1>
<p>Congratulations on building your first network automation tool. This script has the potential to save you significant time and serves as a foundation for more advanced automation tasks.</p>
<p>In next week's post, we will explore Network APIs and discuss how to manage network devices using modern API-first approaches on Juniper platforms.</p>
<h1>Download the code</h1>
<p>All the code from this post is available on GitLab. : <a href="https://gitlab.com/kgosileburu/network-automation-scripts">network-automation-week-1-part2</a></p>
<h1>Questions or Feedback?</h1>
<p>Connect with me on LinkedIn. I'd love to hear about your automation journey!</p>
]]></content:encoded></item><item><title><![CDATA[Introduction to Network Automation: Build Your First Backup Script with Python for a multi-vendor environment - Part 1]]></title><description><![CDATA[Welcome to the first post in our Network Automation series! If you're a network engineer who's been manually logging into devices to grab configurations, this post is for you. Today, we're diving into]]></description><link>https://codednetwork.com/introduction-to-network-automation-build-your-first-backup-script-with-python-for-a-multi-vendor-environment-part-1</link><guid isPermaLink="true">https://codednetwork.com/introduction-to-network-automation-build-your-first-backup-script-with-python-for-a-multi-vendor-environment-part-1</guid><category><![CDATA[NetworkAutomation]]></category><category><![CDATA[Cisco]]></category><category><![CDATA[Juniper]]></category><category><![CDATA[nokia]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Netmiko]]></category><category><![CDATA[junos]]></category><category><![CDATA[Python 3]]></category><dc:creator><![CDATA[Kgosi Leburu]]></dc:creator><pubDate>Tue, 03 Feb 2026 07:34:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768887748566/2427f080-f50d-4cf3-b539-402b6a3fafe1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the first post in our Network Automation series! If you're a network engineer who's been manually logging into devices to grab configurations, this post is for you. Today, we're diving into practical network automation by building a Python script that automatically backs up configurations from Nokia SROS, Cisco IOS and Juniper Junos devices.</p>
<p>By the end of this post, you'll have a working script that can save you hours of repetitive work and provide a solid foundation for your automation journey.</p>
<h1>Why Network Automation?</h1>
<p>Before we jump into code, let's address the elephant in the room: why automate?</p>
<ul>
<li><p><strong>Time Savings</strong>: Manually backing up 50 devices is time-consuming, taking hours, whereas automation completes the task in minutes.</p>
</li>
<li><p><strong>Consistency</strong>: Scripts execute tasks without missing steps or making typographical errors.</p>
</li>
<li><p><strong>Audit Trail</strong>: Automated backups with timestamps provide a dependable change history.</p>
</li>
<li><p><strong>Disaster Recovery</strong>: Regular automated backups ensure preparedness for unforeseen events.</p>
</li>
<li><p><strong>Scalability</strong>: As your network expands, automation prevents manual processes from becoming a bottleneck.</p>
</li>
</ul>
<h1>Why Coding is Essential for Network Engineers Today?</h1>
<p>Modern networks are no longer configured on a device-by-device basis. They are <strong>software-driven systems</strong>. Acquiring coding skills enables network engineers to transition from <em>manual operators to automation engineers.</em></p>
<p>Here’s what coding enables:</p>
<ol>
<li><p><strong>Automation Over Repetition</strong></p>
<p>Instead of logging into 100 devices:</p>
<ul>
<li><p>A script can push configs</p>
</li>
<li><p>Validate state</p>
</li>
<li><p>Roll back safely</p>
</li>
</ul>
</li>
<li><p><strong>Expedited Troubleshooting and Enhanced Visibility</strong></p>
<p>With code, you can:</p>
<ul>
<li><p>Pull live data from devices</p>
</li>
<li><p>Parse outputs (JSON/XML/YANG)</p>
</li>
<li><p>Detect anomalies automatically</p>
</li>
</ul>
</li>
<li><p><strong>Vendor-Agnostic Networking</strong></p>
<p>Coding helps you think in <strong>models</strong>, not commands:</p>
<ul>
<li><p>YANG models</p>
</li>
<li><p>OpenConfig</p>
</li>
<li><p>REST / NETCONF / gNMI</p>
</li>
</ul>
</li>
<li><p><strong>Career Longevity and Growth</strong></p>
<p>Roles that increasingly expect coding skills:</p>
<ul>
<li><p>Network Automation Engineer</p>
</li>
<li><p>NetDevOps Engineer</p>
</li>
<li><p>SRE (Network-focused)</p>
</li>
<li><p>Cloud Network Engineer</p>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>"You're either the one who creates the automation, or you're the one being automated." ~ Tom Preston-Werner, co-founder and former CEO of GitHub</p>
</blockquote>
<h1>Prerequisites</h1>
<p>Before we begin, ensure you have:</p>
<ul>
<li><p>Python 3.8 or higher installed</p>
</li>
<li><p>Basic understanding of Python (variables, functions, loops)</p>
</li>
<li><p>Access to network devices (lab or production)</p>
</li>
<li><p>SSH enabled on your network devices</p>
</li>
</ul>
<h1>Setting Up Your Environment</h1>
<h2>Step 1: Create a Virtual Environment</h2>
<p>Virtual environments keep your project dependencies isolated and clean.</p>
<pre><code class="language-bash"># Create a new directory for your project
mkdir network-automation
cd network-automation
# Create a virtual environment
python3 -m venv venv
# Activate the virtual environment
# On Linux/Mac:
source venv/bin/activate
# On Windows:
venv\Scripts\activate
</code></pre>
<h2>Step 2: Install the Required Libraries</h2>
<p>We'll use Netmiko , a popular Python library that simplifies SSH connections to network devices.</p>
<pre><code class="language-python">pip3 install netmiko
</code></pre>
<h2>Step 3: Set Up a Lab Environment for Testing</h2>
<p>In my testing environment, I am utilizing <strong>Containerlab</strong>. <strong>Containerlab</strong> is an open-source tool designed for creating network topologies with containerized network devices. It allows you to quickly set up multi-vendor network labs on your laptop without the need for hypervisors or heavy virtual machines, using only lightweight containers.</p>
<h3>Multi-Vendor Network Topology:</h3>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768958379269/c2c464cd-e932-44c8-9acc-5f0b4091eefb.png" alt="" style="display:block;margin-left:auto" />

<h1>Understanding Netmiko</h1>
<p><strong>Netmiko</strong> is a <strong>Python library</strong> that simplifies SSH connections to network devices, making it easier for network engineers to automate tasks without needing deep software engineering skills</p>
<p>Think of Netmiko as:</p>
<blockquote>
<p>“SSH for network engineers, done the right way.”</p>
</blockquote>
<h2>What issues does Netmiko address?</h2>
<p>When you manually SSH into a router, you must manage prompts, paging, privilege modes, and command timing on your own. Netmiko automates these processes. It is designed to interact seamlessly with devices from Nokia, Cisco, Juniper, Arista, HP, and many other vendors, eliminating the need to write custom code for each one.</p>
<h2>Key features</h2>
<ul>
<li><p><strong>Automatic handling of prompts</strong>: Knows when commands finish executing</p>
</li>
<li><p><strong>Paging disabled</strong>: Automatically handles "-- More --" prompts</p>
</li>
<li><p><strong>Multiple vendors</strong>: Just change <code>device_type</code> to support different platforms</p>
</li>
<li><p><strong>Configuration mode</strong>: Built-in methods for entering config mode and sending commands</p>
</li>
<li><p><strong>Error handling</strong>: Can detect command failures</p>
</li>
</ul>
<h1>Building Your First Backup Script</h1>
<h2>Version 1: Single Device Backup</h2>
<p>Let's begin with a simple script to back up a single Nokia SROS device.</p>
<pre><code class="language-python">from netmiko import ConnectHandler
from datetime import datetime
import os
import sys

# Device details
nokia_device = {
    'device_type': 'alcatel_sros',
    'host': '172.20.20.13',
    'username': 'admin',
    'password': 'admin',
    'timeout': 60,  # Increased timeout for large configs
    'session_log': 'netmiko_session.log'  # Optional: log session for debugging
}

# Commands to execute
commands = [
    'environment no more',
    'admin display-config'
]

# Create backup directory if it doesn't exist
backup_dir = 'backups'
if not os.path.exists(backup_dir):
    os.makedirs(backup_dir)
    print(f"Created backup directory: {backup_dir}")

def validate_config(config_text):
    """Validate that configuration was retrieved successfully"""
    if not config_text or len(config_text) &lt; 100:
        return False, "Configuration appears empty or too short"
    
    # Check for common SR OS config indicators
    if 'configure' not in config_text.lower():
        return False, "Configuration doesn't appear to be valid SR OS config"
    
    return True, "Configuration validated"

try:
    # Connect to device
    print(f"Connecting to {nokia_device['host']}...")
    connection = ConnectHandler(**nokia_device)
    print("Connected successfully")
    
    # Get hostname for filename
    hostname = connection.find_prompt().strip('#&gt; ')
    print(f"Device hostname: {hostname}")
    
    # Execute commands
    print("Setting terminal parameters...")
    connection.send_command(commands[0], expect_string=r'#')
    
    print(f"Retrieving configuration from {hostname}...")
    config = connection.send_command(commands[1], expect_string=r'#', delay_factor=2)
    
    # Validate configuration
    is_valid, message = validate_config(config)
    if not is_valid:
        print(f"Warning: {message}")
        response = input("Continue anyway? (y/n): ")
        if response.lower() != 'y':
            print("Backup cancelled")
            connection.disconnect()
            sys.exit(1)
    else:
        print(message)
    
    # Create timestamp for filename
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    
    # Create filename
    filename = f"{backup_dir}/{hostname}_{timestamp}.txt"
    
    # Save configuration to file
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(f"# Backup created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"# Device: {hostname} ({nokia_device['host']})\n")
        f.write(f"# {'='*60}\n\n")
        f.write(config)
    
    # Verify file was created and has content
    file_size = os.path.getsize(filename)
    print(f"Configuration saved to {filename}")
    print(f"File size: {file_size:,} bytes")
    
    # Disconnect
    connection.disconnect()
    print("Disconnected successfully")
    
except ConnectionError as e:
    print(f"Connection error: {str(e)}")
    print("Check network connectivity and device accessibility")
    sys.exit(1)
    
except Exception as e:
    print(f"An error occurred: {str(e)}")
    print(f"Error type: {type(e).__name__}")
    sys.exit(1)

print("\nBackup completed successfully!")
</code></pre>
<h2>Expected Output</h2>
<pre><code class="language-python"># Execute the script 
python3 backup.py

Connecting to 172.20.20.13...
Connected successfully
Device hostname: *A:PE1
Setting terminal parameters...
Retrieving configuration from *A:PE1...
Configuration validated
Configuration saved to backups/*A:PE1_20260121_143523.txt
File size: 5,729 bytes
Disconnected successfully

Backup completed successfully!
</code></pre>
<h1>What’s Next?</h1>
<p>You now have a working understanding of SSH connections and basic command execution with Netmiko. In Part 2, we'll use these skills to build a practical script that backs up configurations from multiple devices automatically—the kind of task that saves network engineers hours every week. We will also breakdown the code step-by-step. Even if you've never programmed before, you'll understand exactly how it works.</p>
<p>By the end, you'll have a script that automatically backs up configurations from Nokia, Cisco, and Juniper devices!</p>
<h1>Download the code</h1>
<p>All the code from this post is available on GitLab: <a href="https://gitlab.com/kgosileburu/network-automation-scripts">network-automation-week-1</a></p>
<h1>Questions or Feedback?</h1>
<p>Connect with me on LinkedIn. I'd love to hear about your automation journey!</p>
]]></content:encoded></item></channel></rss>