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; noconfigure
—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
- Setup & Load: Creds/paths (update lines 10-15).
load_commands
pulls your sets (skips comments). - Core Deploy (
deploy_config
): SSH, pager off, config in, loop sends (3s timeout each), commit (15s buffer), exit. Cleans ANSI, saves structured log. - Main: Validates, previews commands, threads across FWs, tallies wins.
Running the Deploy
- Update creds/paths.
- Prep files as above.
- Launch:
python palo_alto_config_deploy.py
. - 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 inoutput += send_and_capture(shell, 'show config diff')
. - PAN-OS Notes: 10.x+; if prompts linger, add
shell.send('yes\n')
after commit.