Automating a Legacy Cisco Switch with Ansible

My access switch is a Cisco 2960G. It’s a solid gigabit switch that has been running for the better part of two decades, and I see no reason to replace hardware that works. Modern automation tools disagree. Getting Ansible to manage it turned into a small archaeology project, and since half the enterprise world still runs gear like this, the fixes are worth writing down.

Wall one: SSH won’t even connect

New SSH clients have dropped the key exchange and cipher algorithms old IOS speaks, so the connection dies before login with errors about no matching kex or cipher. The fix is re-enabling the legacy crypto, scoped to just the hosts that need it:

# ansible.cfg
[defaults]
vault_password_file = ~/.ansible_vault_pass
host_key_checking = False        # lab only; leave on in prod

[ssh_connection]
ssh_args = -o KexAlgorithms=+diffie-hellman-group14-sha1 -o HostKeyAlgorithms=+ssh-rsa -o Ciphers=+aes256-cbc

You’re explicitly re-enabling algorithms that were retired for a reason, so scope it. If the config holds modern gear too, put the legacy options in an ~/.ssh/config Host block matched to the old devices instead of globally in ansible.cfg.

Wall two: the library underneath

Newer versions of paramiko, the SSH library Ansible leans on for network gear, also dropped legacy support. The connection failures this causes say nothing about versions, which is why it cost me more time than I’d like to admit. The fix is one pin:

pip install 'paramiko<3.0'

With both walls down, a normal network_cli playbook works like the switch was born yesterday:

- name: Baseline the 2960G
  hosts: access_switches
  gather_facts: no
  connection: ansible.netcommon.network_cli
  vars:
    ansible_network_os: cisco.ios.ios
  tasks:
    - name: Pull facts
      cisco.ios.ios_facts:
        gather_subset: all

    - name: Save running config for the backup job
      cisco.ios.ios_command:
        commands: show running-config
      register: runcfg

The smaller potholes from the same project

vault_password_file belongs in the [defaults] section of ansible.cfg, not wherever you happen to be when you get tired of typing --ask-vault-pass.

delegate_to: localhost takes your hostvars context with it. If a task needs facts from the switch while running locally, capture them first:

- set_fact:
    switch_version: "{{ ansible_net_version }}"

- name: Do something locally with it
  delegate_to: localhost
  debug:
    msg: "Switch runs {{ switch_version }}"

And extra_vars cannot be set in ansible.cfg at all, no matter how much sense that would make. Command line or playbook, nowhere else.

People ask why I bother automating a lab switch when I could type the six commands. Because these gotchas are the same ones waiting in any environment running older gear, and I’d rather meet them at home than during a maintenance window. Old hardware is the best teacher precisely because nothing works out of the box.