Every network automation tutorial on the internet starts the same way: "first, install Python on your laptop." This one does not. In this lab you build the automation host inside the network, on the same Alpine Linux node that ships with the PingLabz CCNA Automation Labs topology. By the end, a Python script running on HOST1 will SSH into R1 and pull live output from a router you can see booting in Cisco Modeling Labs. This is the first lab in the automation series, it is completely free, and everything below was captured from the real lab: Alpine 3.23, Python 3.12, Netmiko 4.7, and Cisco IOS XE 17.18.
Automation and Programmability is Domain 6 of the CCNA 200-301 blueprint, worth 10% of your exam. It is also the domain most CCNA candidates study entirely from flashcards. The difference between memorizing "REST APIs use HTTP verbs" and having actually pushed a command to a router from Python is the same difference the other 90% of the exam rewards: hands-on beats theory recall, every time.
The topology
This series runs on the PingLabz CCNA Automation Lab topology, a variant of the same five-node base used across the PingLabz lab library. It fits Cisco Modeling Labs Free: five counted nodes, plus an unmanaged switch and an external connector, neither of which counts toward the CML Free limit.
| Node | Platform | Role | Management IP |
|---|---|---|---|
| R1 | iol-xe (IOS XE 17.18) | LAN gateway | 10.20.0.1 |
| R2 | iol-xe | Transit router | 10.20.0.2 |
| R3 | iol-xe | Remote router | 10.30.30.2 |
| SW1 | ioll2-xe | Managed L2 switch | 10.20.0.10 |
| HOST1 | Alpine Linux | The automation host (you build it here) | 10.20.0.50 |
| SW2 | unmanaged switch | Spare broadcast domain (not counted) | n/a |
| EXT-NAT | external connector | NAT internet for HOST1 (not counted) | n/a |
The external connector is the one piece you have not seen in the other PingLabz series. HOST1 needs to download packages from the Alpine mirrors, so its second interface (eth1) connects to an External Connector in NAT mode. CML gives that interface outbound internet through your computer's own connection, with no configuration on your side. Per the Cisco CML documentation, external connectors do not count toward the node license, so the lab still fits CML Free.
What you will learn
- How to set up an Alpine Linux node in CML as a network automation host, the right way (let it boot with its default config first).
- How to give a Linux host dual-homed networking: one interface for internet, one for the lab management network.
- How to enable SSH version 2 on Cisco IOS XE with modern 3072-bit RSA keys, and why the old config-mode keygen command is deprecated.
- How to install Netmiko in a Python virtual environment and make your first programmatic connection to a router.
Download the topology
Import the .yaml into CML (Import Lab in the dashboard), hit start, and give the nodes a minute to boot. The routers and switch boot with the standard PingLabz base configuration. Router login is pinglabz / PingLabz!23 with enable secret Cisco@123.
Step 1: Let HOST1 boot, then log in
One rule for Alpine nodes in CML, learned the hard way: let the node boot completely with its default configuration before you touch it. Do not attach a day-0 config, and do not start typing into the console while it is still booting. Give it a minute after the lab starts, then open the HOST1 console and log in with CML's Alpine defaults: username cisco, password cisco.
HOST1 login: cisco
Password:
HOST1:~$ whoami
cisco
HOST1:~$ cat /etc/alpine-release
3.23.3The cisco user has passwordless sudo, which you will use for everything that touches the network stack or the package manager.
Step 2: Get internet on eth1
HOST1 has two interfaces. eth0 is wired to SW1 (the lab LAN); eth1 is wired to the EXT-NAT external connector. Bring eth1 up and ask for a DHCP lease:
HOST1:~$ sudo ip link set eth1 up
HOST1:~$ sudo udhcpc -i eth1 -n -q
udhcpc: started, v1.37.0
udhcpc: broadcasting discover
udhcpc: broadcasting select for 192.168.255.24, server 192.168.255.1
udhcpc: lease of 192.168.255.24 obtained from 192.168.255.1, lease time 3600The -n flag makes udhcpc exit if no lease appears (instead of retrying forever in your console), and -q quits once the lease is bound. CML's NAT connector hands out addresses from its internal 192.168.255.0/24 pool and NATs everything outbound. Confirm you can reach the internet:
HOST1:~$ ping -c 2 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=42 time=12.324 ms
64 bytes from 8.8.8.8: seq=1 ttl=42 time=13.307 msStep 3: Address eth0 for the lab LAN
eth0 talks to the lab. Give it the standard PingLabz automation-host address, plus one static route so HOST1 can reach R3's side of the point-to-point link through R2:
HOST1:~$ sudo ip addr add 10.20.0.50/24 dev eth0
HOST1:~$ sudo ip route add 10.30.30.0/30 via 10.20.0.2
HOST1:~$ traceroute -n -m 4 10.30.30.2
traceroute to 10.30.30.2 (10.30.30.2), 4 hops max, 46 byte packets
1 10.20.0.2 3.433 ms 2.823 ms 4.659 ms
2 10.30.30.2 4.133 ms * 5.199 msTwo hops: HOST1 to R2, R2 across the P2P link to R3. The default route stays on eth1 (internet); only the lab prefixes use eth0. This is the same split-management pattern you will meet in production, where the automation host has one leg in the management VLAN and one leg toward the wider network.
Runtime ip commands do not survive a reboot. To make the addressing permanent, write it into Alpine's /etc/network/interfaces:
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 10.20.0.50/24
post-up ip route add 10.30.30.0/30 via 10.20.0.2 || true
auto eth1
iface eth1 inet dhcpStep 4: Install Python and Netmiko
Alpine ships lean: no Python on board. Two packages from the mirrors fix that:
HOST1:~$ sudo apk update
v3.23.4-359-g5b71e521e31 [https://dl-cdn.alpinelinux.org/alpine/v3.23/main]
v3.23.4-359-g5b71e521e31 [https://dl-cdn.alpinelinux.org/alpine/v3.23/community]
OK: 27629 distinct packages available
HOST1:~$ sudo apk add python3 py3-pip
(9/20) Installing python3 (3.12.13-r0)
...
OK: 138.1 MiB in 123 packages
HOST1:~$ python3 --version
Python 3.12.13Install Netmiko inside a virtual environment. Modern Python distributions protect the system site-packages, and a venv keeps your automation dependencies isolated and reproducible (the same discipline you will want on a production jump host):
HOST1:~$ python3 -m venv ~/autolab
HOST1:~$ . ~/autolab/bin/activate
(autolab) HOST1:~$ pip install netmiko
(autolab) HOST1:~$ python3 -c "import netmiko; print('netmiko', netmiko.__version__)"
netmiko 4.7.0Step 5: Enable SSH on R1, the modern way
Netmiko talks SSH, and the routers boot with SSH allowed on the VTY lines but no host keys generated. On the R1 console, generate the keys from privileged EXEC mode. Watch what IOS XE 17.x says if you try the old way first:
R1(config)# crypto key generate rsa modulus 2048
%This command is deprecated. Use the command in exec mode instead
...
SECURITY WARNING - Module: SSH, Command: crypto key generate rsa ...,
Reason: SSH host key uses insufficient key length,
Description: SSH with insufficient key length,
Remediation: Use SSH RSA host key with a minimum length of 3072 bits for enhanced securityTwo lessons in one capture: config-mode keygen is deprecated, and 2048-bit RSA now trips a security warning. Do it properly, from EXEC mode, at 3072 bits, then pin SSH to version 2:
R1# crypto key generate rsa modulus 3072
% The key modulus size is 3072 bits
% Generating crypto RSA keys in background ...
R1# configure terminal
R1(config)# ip ssh version 2
R1# show ip ssh
SSH Enabled - version 2.0
Authentication methods:publickey,keyboard-interactive,password
Encryption Algorithms:chacha20-poly1305@openssh.com,aes128-gcm@openssh.com,aes256-gcm@openssh.com, ...
Minimum expected Diffie Hellman key size : 2048 bitsSanity-check the path from HOST1 with a plain SSH client before involving Python (sudo apk add openssh-client if you have not already):
HOST1:~$ ssh pinglabz@10.20.0.1 'show ip interface brief'
(pinglabz@10.20.0.1) Password:
*****************************************************
* PingLabz CCNA Automation Lab - R1 *
* https://www.pinglabz.com/automation-labs/ *
* Authorized practice use only *
*****************************************************
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 10.20.0.1 YES TFTP up up
Ethernet0/1 unassigned YES unset administratively down down
Ethernet0/2 unassigned YES unset administratively down down
Ethernet0/3 unassigned YES unset administratively down down
Loopback0 10.255.0.1 YES TFTP up up(The Method column says TFTP because CML injects the startup configuration at boot; on physical hardware you would normally see NVRAM or manual.)
Step 6: Your first Netmiko script
Now replace yourself with Python. Create hello_netmiko.py on HOST1:
from netmiko import ConnectHandler
r1 = {
"device_type": "cisco_ios",
"host": "10.20.0.1",
"username": "pinglabz",
"password": "PingLabz!23",
"secret": "Cisco@123",
}
conn = ConnectHandler(**r1)
print(conn.find_prompt())
print(conn.send_command("show version | include uptime"))
conn.disconnect()Run it:
(autolab) HOST1:~$ python3 hello_netmiko.py
R1#
R1 uptime is 17 minutesThat is the whole trick. ConnectHandler opens the SSH session and handles the prompt detection, find_prompt() proves you landed where you think you did, and send_command() sends one exec command and returns its output as a Python string. Everything else in network automation is this, repeated, with better error handling.
Troubleshooting matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Cannot log in to HOST1 console | Touched the node before first boot completed, or a day-0 config replaced the defaults | Wipe the node (not the lab) and let it boot untouched, then cisco/cisco |
udhcpc: sendto: Network is down | eth1 link is still down | sudo ip link set eth1 up first, then udhcpc |
socket: Operation not permitted | Ran a network command without root | Prefix with sudo |
| apk cannot reach mirrors | eth1 has no lease, or the EXT-NAT connector is not in NAT mode | Verify the lease with ip addr show eth1; check the connector's Config tab says NAT |
| Netmiko times out connecting | SSH keys never generated on R1, or eth0 has no address | show ip ssh on R1 must say Enabled; ip addr show eth0 on HOST1 |
| Netmiko authentication failure | Typo in username/password dictionary | Credentials are pinglabz / PingLabz!23, secret Cisco@123 |
Key takeaways
- The automation host lives inside the topology: Alpine + venv + Netmiko, fed by a NAT external connector that does not count against CML Free's five nodes.
- Let Alpine boot with its default config before touching it. Login is cisco/cisco with passwordless sudo.
- IOS XE 17.x wants RSA keys generated in EXEC mode at 3072 bits minimum; the config-mode command is deprecated and 2048-bit keys trigger a security warning.
- One Netmiko pattern (ConnectHandler, find_prompt, send_command) is the foundation for every script in the rest of this series.
Next up: Lab auto-02, Reading the Network with Netmiko, where one script interrogates all four devices and you stop opening consoles for routine checks. The full series index lives at /automation-labs/.