Home > Networking > Automating Palo Alto Firewall Configs: Bulk Deployment of CLI Commands with Python and Paramiko

Automating Palo Alto Firewall Configs: Bulk Deployment of CLI Commands with Python and Paramiko

Tired of SSHing into every Palo Alto firewall in your stack to push the same set of config changes? Whether it’s adding an address object and tossing it into a group, tweaking NAT rules, or rolling out policy tweaks across a fleet, manual CLI work scales like a bad joke. Enter Python and Paramiko: a no-frills combo for SSH automation that reads your commands from a simple text file, blasts them into config mode, commits (with auto-yes on prompts), and logs everything per-device in threaded glory.

This script’s your bulk deploy sidekick—load firewalls from a TXT list (pa_firewalls.txt), prep your sets in deploy_commands.txt (e.g., set address IP-Test address 192.168.1.100\nset address-group My-Group member IP-Test), and watch it parallelize across 10+ FWs in minutes. No menus, no fluff—just apply and audit. Ideal for your initial test (one address + group add) or scaling to full change windows. Let’s deploy like pros.

Why Automate CLI Deploys on Palo Alto?

PAN-OS CLI is powerful for configs (set/commit), but repetitive? It’s a time sink. This script:

  • Bulk Handles Fleets: Threaded for speed (tune to your AAA tolerance).
  • Command File-Driven: Edit deploy_commands.txt without recoding—version it in Git.
  • Safe Logging: Per-FW outputs in D:\python\ (timestamped, with commands + CLI responses). Grep for “SUCCESS” to verify commits.
  • PAN-OS Native: Stays in CLI (no API keys needed). Handles prompts, ANSI junk, and paging.

Pro: Full control over sets (e.g., vsys-aware). Con: Test single-FW first (set max_workers=1)—commits are live.

Prerequisites

  • Python 3.x: With pip install paramiko.
  • SSH Access: Superuser CLI on FWs (management interface enabled).
  • Files:
  • pa_firewalls.txt: One IP per line (e.g., 192.168.1.1\n10.0.0.5).
  • deploy_commands.txt: Your sets (one per line; no configure—script adds it). Example for your test:
    set address IP-Test address 192.168.1.100 set address-group My-Group member IP-Test
  • Output Dir: D:\python\ (auto-created for logs).

Security: Hardcoded creds for demo—swap for env vars (os.getenv) in prod. Run from a bastion.

The Script: Command-File Bulk Deployer

Save as palo_alto_config_deploy.py. It loads, threads, applies, commits, and logs—ready for your address/group push.

import paramiko
import sys
import time
import os
import re  # For ANSI stripping
from datetime import datetime  # For timestamped filenames
from concurrent.futures import ThreadPoolExecutor, as_completed  # For multi-threading

# HARDCODED CREDENTIALS - UPDATE THESE
username = 'your_username'  # e.g., 'admin'
password = 'your_password'  # e.g., 'Pa55w0rd!'

# Paths - UPDATE IF NEEDED
ips_file_path = r'C:\path\to\your\pa_firewalls.txt'  # FW IPs, one per line
commands_file_path = r'C:\path\to\your\deploy_commands.txt'  # Set commands, one per line

# Local output directory - creates if missing
output_dir = r'D:\python'
os.makedirs(output_dir, exist_ok=True)

# THREADING CONFIG - ADJUST HERE
max_workers = 5  # Concurrent SSH; tune for AAA

def load_commands(commands_path):
    """
    Load commands from file, strip extras.
    """
    if not os.path.exists(commands_path):
        raise FileNotFoundError(f"Commands file missing: {commands_path}")
    with open(commands_path, 'r') as f:
        return [line.strip() for line in f if line.strip() and not line.startswith('#')]

def send_and_capture(shell, command, timeout=5):
    """
    Send command, capture output.
    """
    shell.send(command + '\n')
    time.sleep(timeout)

    output = ''
    max_wait = 30
    wait_count = 0
    while wait_count < max_wait:
        if shell.recv_ready():
            output += shell.recv(4096).decode('utf-8')
            wait_count = 0
        else:
            time.sleep(0.1)
            wait_count += 1
    return output

def deploy_config(ip, username, password, commands):
    """
    SSH to FW, enter config, run commands, commit, save output.
    """
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    filename = f"{ip.replace('.', '_')}_deploy_{timestamp}.txt"
    filepath = os.path.join(output_dir, filename)

    try:
        client.connect(hostname=ip, username=username, password=password, timeout=10, look_for_keys=False, allow_agent=False)
        shell = client.invoke_shell()
        time.sleep(2)

        # Disable paging
        shell.send('set cli pager off\n')
        time.sleep(1)
        while not shell.recv_ready():
            time.sleep(0.1)
        shell.recv(4096).decode('utf-8')

        # Enter config mode
        shell.send('configure\n')
        time.sleep(2)
        output = ''
        while not shell.recv_ready():
            time.sleep(0.1)
        output += shell.recv(4096).decode('utf-8')

        # Run commands
        for cmd in commands:
            output += send_and_capture(shell, cmd, timeout=3)

        # Commit (auto-yes)
        output += send_and_capture(shell, 'commit', timeout=15)
        # Handle potential yes prompt (network buffer covers)
        time.sleep(5)  # Drain commit progress
        while shell.recv_ready():
            output += shell.recv(4096).decode('utf-8')
            time.sleep(0.1)

        # Exit config
        output += send_and_capture(shell, 'exit\n', timeout=2)

        # Clean exit
        shell.send('exit\n')
        time.sleep(1)
        client.close()

        # Clean ANSI
        output = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', output)

        with open(filepath, 'w') as f:
            f.write(f"=== DEPLOY LOG FOR {ip} ===\nCommands Applied:\n" + '\n'.join(commands) + f"\n\nOutput:\n{output}")

        line_count = len(output.splitlines())
        print(f"Deploy saved for {ip}: {filepath} ({line_count} lines)")
        return filepath

    except Exception as e:
        print(f"Error on {ip}: {e}")
        return None
    finally:
        try:
            client.close()
        except:
            pass

def main():
    # Validate files
    if not os.path.exists(ips_file_path):
        print(f"ERROR: IPs file missing: {ips_file_path}")
        sys.exit(1)

    commands = load_commands(commands_file_path)
    if not commands:
        print("ERROR: No commands in file.")
        sys.exit(1)

    with open(ips_file_path, 'r') as f:
        fw_ips = [line.strip() for line in f if line.strip()]

    if not fw_ips:
        print("ERROR: No IPs in file.")
        sys.exit(1)

    if username == 'your_username' or password == 'your_password':
        print("ERROR: Update credentials (lines 10-11).")
        sys.exit(1)

    print(f"Loaded {len(fw_ips)} firewalls from {ips_file_path}")
    print(f"Loaded {len(commands)} commands from {commands_file_path}")
    print("Sample Commands:")
    for i, cmd in enumerate(commands[:3], 1):
        print(f"  {i}. {cmd}")
    if len(commands) > 3:
        print(f"  ... and {len(commands)-3} more")
    print(f"\nStarting threaded deploys (using {max_workers} threads)...\n")

    success_count = 0
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_ip = {executor.submit(deploy_config, ip, username, password, commands): ip for ip in fw_ips}
        for future in as_completed(future_to_ip):
            ip = future_to_ip[future]
            try:
                result = future.result()
                if result:
                    success_count += 1
            except Exception as exc:
                print(f"Thread exception for {ip}: {exc}")

    print(f"\nDeploy complete: {success_count}/{len(fw_ips)} successful.")
    print("Check D:\\python for logs (grep for 'Commit job' or errors).")

if __name__ == "__main__":
    main()

Quick Breakdown

  1. Setup & Load: Creds/paths (update lines 10-15). load_commands pulls your sets (skips comments).
  2. Core Deploy (deploy_config): SSH, pager off, config in, loop sends (3s timeout each), commit (15s buffer), exit. Cleans ANSI, saves structured log.
  3. Main: Validates, previews commands, threads across FWs, tallies wins.

Running the Deploy

  1. Update creds/paths.
  2. Prep files as above.
  3. Launch: python palo_alto_config_deploy.py.
  4. Sample Output:
   Loaded 3 firewalls from C:\path\to\your\pa_firewalls.txt
   Loaded 2 commands from C:\path\to\your\deploy_commands.txt
   Sample Commands:
     1. set address IP-Test address 192.168.1.100
     2. set address-group My-Group member IP-Test

   Starting threaded deploys (using 5 threads)...

   Deploy saved for 192.168.1.1: D:\python\192_168_1_1_deploy_20251018_143022.txt (234 lines)
   Deploy saved for 10.0.0.5: D:\python\10_0_0_5_deploy_20251018_143023.txt (198 lines)
   Error on 10.0.0.6: Timeout

   Deploy complete: 2/3 successful.
   Check D:\python for logs (grep for 'Commit job' or errors).

Log File Snippet:

   === DEPLOY LOG FOR 192.168.1.1 ===
   Commands Applied:
   set address IP-Test address 192.168.1.100
   set address-group My-Group member IP-Test

   Output:
   IP-Test {
       address 192.168.1.100;
   }
   My-Group {
       member [ IP-Test ];
   }
   Commit job 42 finished. Status: SUCCESS

Pro Tips

  • Bulk Commands: For 30+ sets, bump timeout to 5s in loop (line ~70). Test commit logs for “SUCCESS”.
  • Vsys/Multi-Tenancy: Prefix commands: set vsys 1 address ....
  • Rollback: Add delete address IP-Test to a rollback file; run post-test.
  • Enhance: Pre-run show config diff? Slot in output += send_and_capture(shell, 'show config diff').
  • PAN-OS Notes: 10.x+; if prompts linger, add shell.send('yes\n') after commit.

Leave a Comment