Ansible — Automating a Fleet, Not One Device

25 min In Progress

Learning Objectives

  • Explain the difference between connecting to one device and describing a fleet-wide end state
  • Define idempotent and recognize it in running code
  • Build a fleet runner that applies a rule across many simulated devices

Why Ansible — Thinking in Fleets

Paramiko and Netmiko connect to one device per script run. Ansible flips the approach entirely: instead of scripting steps, you describe the end state you want, and it figures out how to get every device in the inventory there. The other defining feature is idempotency — running the same playbook twice should never produce a different result the second time.

The Real Driver (Lab / Real-Device Code)

A real Ansible setup is two files (an inventory and a playbook) plus, if you want to trigger it from Python, a thin wrapper around ansible_runner.

Lab-only: the top two blocks are YAML and INI, not Python — running this here raises a SyntaxError, which is expected. The Python class also needs `pip install ansible-runner` plus real playbook/inventory files and reachable devices.
# inventory.ini
[cisco_routers]
192.168.1.1 ansible_network_os=ios ansible_user=admin ansible_password=secret

# backup.yml
- name: Backup router configs
  hosts: cisco_routers
  gather_facts: no
  tasks:
    - name: Run show running-config
      ios_command:
        commands: show running-config
      register: config_output
    - name: Save to file
      copy:
        content: "{{ config_output.stdout[0] }}"
        dest: "backups/{{ inventory_hostname }}.cfg"

# Driving it from Python:
import ansible_runner

class ansible_fleet_runner:
    def __init__(self, playbook_path, inventory_path):
        self.playbook_path = playbook_path
        self.inventory_path = inventory_path

    def run(self):
        result = ansible_runner.run(
            private_data_dir='.',
            playbook=self.playbook_path,
            inventory=self.inventory_path,
        )
        return result.status, result.rc

runner = ansible_fleet_runner('backup.yml', 'inventory.ini')
print(runner.run())

Practice Safely — Simulated Fleet Runner

No YAML engine here — just the concept that matters: one rule, applied to every device, with each device deciding for itself whether a change is even needed. Run this twice in your head and notice the second run should report no changes anywhere.

class simulated_fleet_device:
    def __init__(self, name, running_config):
        self.name = name
        self.running_config = running_config

    def apply_if_missing(self, required_line):
        if required_line in self.running_config:
            return f'{self.name}: already present -- {required_line}'
        self.running_config.append(required_line)
        return f'{self.name}: ADDED -- {required_line}'


class fleet_runner:
    """Stands in for an Ansible playbook run: the same task, applied
    across every device in the inventory, idempotently."""

    def __init__(self, devices):
        self.devices = devices

    def run_playbook(self, required_line):
        return [device.apply_if_missing(required_line) for device in self.devices]


fleet = [
    simulated_fleet_device('edge-router-1', ['ip ssh version 2']),
    simulated_fleet_device('edge-router-2', ['ip ssh version 2', 'ntp server 10.0.0.1']),
    simulated_fleet_device('core-switch-1', []),
]

runner = fleet_runner(fleet)

print('First run:')
for line in runner.run_playbook('ntp server 10.0.0.1'):
    print(' ', line)

print()
print('Running the exact same playbook again -- nothing should change:')
for line in runner.run_playbook('ntp server 10.0.0.1'):
    print(' ', line)

📝 Exercise: Run a Two-Rule Golden-Config Audit

Extend fleet_runner.run_playbook() to accept a list of golden rules instead of one line, apply every rule to every device, and print a summary: how many changes were made vs how many devices were already compliant.

code.py
Output

            
← Back to Chapter