Note
This guide covers
paloaltonetworks.panos, the official Ansible Collection from Palo Alto Networks for managing PAN-OS firewalls and Panorama. It is intended for SREs, network automation engineers, and platform teams who use Ansible to drive their infrastructure. The guide is generated against commit0ea91f8(collection version3.3.0) and was written from the in-tree module sources inplugins/modules/.
The collection ships 118 modules that wrap the pan-os-python SDK and the
PAN-OS XML API into idempotent Ansible tasks. You can manage objects (address
objects, address groups, services, tags, schedules), policies (security, NAT,
PBF, decryption), network plumbing (interfaces, zones, virtual routers, static
routes, BGP, IPsec), device administration (admins, certificates, management
profiles), HA, software lifecycle, log forwarding, and Panorama scope (device
groups, templates, template stacks, commit/push). The collection talks to a
firewall or Panorama by IP/hostname plus credentials — no agent, no on-box
software needed.
1. Quick Start
What this is
paloaltonetworks.panos is a Red Hat-certified Ansible Collection authored
by Palo Alto Networks. It wraps pan-os-python (the official PAN-OS SDK) so
that every PAN-OS configuration object becomes a declarative, idempotent
Ansible task. Two-line install, ten-line playbook, and you can drive a
firewall or Panorama from any control node that can reach its management IP
on TCP/443.
Key features:
- 118 modules covering objects, policies, networking, device config, HA, software install, Panorama, and operational commands.
- Idempotent task pattern —
state: present/state: absent/state: gathered/state: replaced/state: merged/state: deletedon the network-resource modules. - Single connection model via the
providerdict (IP + credentials), reusable across every task. - First-class Panorama support:
device_group,template,template_stack,vsysparameters on every applicable module. - Built-in HTTP API plugin (
paloaltonetworks.panos.panos) for inventory-based connection if you prefer thehttpapiconnection style overprovider. - Shipping
event_sourceplugin for Event-Driven Ansible (EDA) — stream PAN-OS logs into rulebooks.
Prerequisites
- Ansible Core 2.16 or newer on the control node (declared in
meta/runtime.yml). - Python 3.10 or newer on the control node (declared in
requirements.txtvia thepython_version >= "3.10"markers). - Network reachability from the control node to the firewall or Panorama management IP on TCP/443.
- A PAN-OS account that can authenticate via the XML API — admin credentials or a pre-generated API key.
Tip
One liner:
pip install pan-os-python panos-upgrade-assurance && ansible-galaxy collection install paloaltonetworks.panos. That gets you the collection plus the SDK plus the upgrade-assurance helper used bypanos_active_in_ha,panos_readiness_checks,panos_snapshot_report, andpanos_state_snapshot.
Install in 30 seconds
ansible-galaxy collection install paloaltonetworks.panos
pip install pan-os-python>=1.12.4 panos-upgrade-assurance>=2.0.0 xmltodict>=0.12.0 dpath>=2.1.5
Minimal playbook
Save the following as firewall-hello.yml and replace the IP/credentials:
---
- name: pan-os-ansible hello world
hosts: localhost
connection: local
gather_facts: false
vars:
provider:
ip_address: "192.0.2.1"
username: "admin"
password: "{{ lookup('env', 'PANOS_PASSWORD') }}"
tasks:
- name: Gather PAN-OS system info
paloaltonetworks.panos.panos_facts:
provider: "{{ provider }}"
gather_subset: ['system']
- name: Add a sample address object
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
name: "Hello-PanOSAnsible"
value: "203.0.113.42"
description: "Created by pan-os-ansible quickstart"
state: present
- name: Commit the candidate config
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
description: "pan-os-ansible quickstart commit"
Run it:
PANOS_PASSWORD='your-pan-password' ansible-playbook firewall-hello.yml
Expected output (abridged):
PLAY [pan-os-ansible hello world] ********************************************
TASK [Gather PAN-OS system info] *********************************************
ok: [localhost]
TASK [Add a sample address object] *******************************************
changed: [localhost]
TASK [Commit the candidate config] *******************************************
changed: [localhost]
PLAY RECAP *******************************************************************
localhost : ok=3 changed=2 unreachable=0 failed=0
Tip
The address-object task is idempotent. Re-running the playbook reports
okinstead ofchangedbecausepanos_address_objectreads the firewall, sees the object already exists with the same value, and does nothing. The same is true for every module in thestate: presentfamily.
What to try next
- Section 2 — install paths beyond
ansible-galaxy(Python virtualenv, Execution Environments, AAP). - Section 3 — every way to feed credentials to the modules (provider dict, classic params, environment variables, Ansible Vault).
- Section 4 — the standard task pattern, idempotency model, and Panorama workflow (commit then push).
- Section 5 — 14 production-shaped recipes covering objects, rules, NAT, HA, syslog, backup, software install, and more.
2. Installation
System requirements
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| Ansible Core | 2.16 | 2.18+ | Declared in meta/runtime.yml |
| Python (control node) | 3.10 | 3.12 | Declared in requirements.txt |
pan-os-python |
1.12.4 | latest 1.x | Pinned <2.0 — the collection uses 1.x APIs |
panos-upgrade-assurance |
2.0.0 | latest 2.x | Required only for the upgrade-assurance modules |
xmltodict |
0.12.0 | latest | Optional but expected by panos_op JSON output |
dpath |
2.1.5 | latest | Used internally by gathered_filter |
aiohttp |
3.8.4 | latest | Used by the EDA event-source plugin |
| PAN-OS device | 8.0 | 10.x or 11.x | Some modules require newer PAN-OS — see per-module notes |
Important
Issue #586 — under Python 3.12 some users hit
Missing required library "pan-os-python"even afterpip install. The cause is almost always Ansible running the module with a different Python interpreter than the one that has the SDK installed. Always verify withansible -m setup localhost | grep ansible_python_versionand pinansible_python_interpreteraccordingly.
Install methods
The collection itself is published on Ansible Galaxy. The Python dependencies are installed separately on the control node because Ansible runs the modules locally (the firewall is contacted by HTTP, not by SSH), so the SDK lives wherever the playbook executor runs.
Method A — Ansible Galaxy (recommended)
ansible-galaxy collection install paloaltonetworks.panos
pip install -r ~/.ansible/collections/ansible_collections/paloaltonetworks/panos/requirements.txt
Pin a specific collection version:
ansible-galaxy collection install paloaltonetworks.panos:==3.3.0
Method B — requirements.yml for project-local installs
# requirements.yml
collections:
- name: paloaltonetworks.panos
version: ">=3.0.0,<4.0.0"
ansible-galaxy collection install -r requirements.yml -p ./collections
Then run playbooks with ANSIBLE_COLLECTIONS_PATH=./collections.
Method C — Python virtual environment for the SDK
python3 -m venv /opt/panos-ansible
source /opt/panos-ansible/bin/activate
pip install --upgrade pip
pip install ansible-core>=2.16 pan-os-python panos-upgrade-assurance xmltodict dpath
ansible-galaxy collection install paloaltonetworks.panos
Set ansible_python_interpreter: /opt/panos-ansible/bin/python in your
inventory so Ansible runs the modules under the right Python.
Method D — Ansible Execution Environment (Ansible Automation Platform)
The collection ships an extensions/ directory describing its EDA contents,
and the source builds cleanly into a container EE. A minimal
execution-environment.yml:
version: 3
images:
base_image:
name: quay.io/ansible/awx-ee:latest
dependencies:
galaxy:
collections:
- name: paloaltonetworks.panos
version: ">=3.0.0"
python:
- pan-os-python>=1.12.4,<2.0
- panos-upgrade-assurance>=2.0.0,<3.0
- xmltodict>=0.12.0
- dpath>=2.1.5
Warning
Issue #630 — Execution Environment builds occasionally fail because Ansible Builder pins old Python deps that conflict with
pan-os-python. Ifansible-builder buildfails on dependency resolution, run with--prune-imagesand bump base image to a recentawx-ee.
Verify the install
ansible-galaxy collection list paloaltonetworks.panos
# /home/ansible/.ansible/collections/ansible_collections
Collection Version
------------------------- -------
paloaltonetworks.panos 3.3.0
Confirm Python deps:
python3 -c "import panos; print(panos.__version__)"
1.12.4
Smoke-test against a firewall:
ansible localhost -m paloaltonetworks.panos.panos_facts \
-a "provider={'ip_address':'192.0.2.1','username':'admin','password':'$PANOS_PASSWORD'} gather_subset=['system']"
Install troubleshooting
Missing required library "pan-python"— install withpip install pan-python(a transitive dep used by the older modules).Missing required library "pan-os-python"— see issue #586. Verify the interpreter Ansible is using.pan-os-python version (X) < minimum version (Y)— bump withpip install --upgrade 'pan-os-python<2.0'. The error path isplugins/module_utils/panos.py.- Collection installs but modules can't be found — the FQCN is
paloaltonetworks.panos.<module_name>. Short module names only work if you setcollections:in your play.
Note
Issue #648 and #655 note that
requirements.txtpins versions tighter than strictly necessary. Newer non-PANW libraries usually work — pip will warn, but the collection still functions.
3. Configuration
The collection is configured at the playbook level. There are no global config files — all behaviour is driven by module parameters or by inventory variables.
Three ways to authenticate
| Method | Where the creds live | When to use |
|---|---|---|
| Provider dict | A provider variable per host or group |
Default. Best for most playbooks. |
| Classic params (deprecated) | ip_address, username, password, api_key, port directly on the task |
Legacy code only — slated for removal in 4.0.0. |
| HTTP API plugin | ansible_connection: httpapi + inventory vars |
Inventory-driven workflows or Ansible Tower native style. |
3.1 Provider dict (recommended)
Every module accepts a provider parameter — a dict of connection details:
provider:
ip_address: "panos.example.com" # required
username: "ansible" # default: admin
password: "{{ vault_panos_pw }}"
api_key: "" # use instead of username/password
port: 443 # default: 443
serial_number: "" # Panorama-managed firewall targeting
The dict is documented in plugins/doc_fragments/fragments.py under
PROVIDER. Then reference it in every task:
- name: Add object
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
name: "Web-Server-A"
value: "10.0.10.20"
Tip
Define
provideringroup_vars/firewalls.ymlso every host inherits it. Override per-host inhost_vars/<hostname>.ymlonly when needed.
3.2 API key instead of password
Generate the API key with the panos_api_key module (or out-of-band via the
firewall's UI / /api/?type=keygen), then use it:
- name: Get API key once
paloaltonetworks.panos.panos_api_key:
provider:
ip_address: "192.0.2.1"
username: "admin"
password: "{{ vault_panos_pw }}"
register: keygen
- name: Use the API key for everything else
paloaltonetworks.panos.panos_address_object:
provider:
ip_address: "192.0.2.1"
api_key: "{{ keygen.api_key }}"
name: "X"
value: "10.0.0.1"
Important
When
api_keyis set,usernameandpasswordare ignored. The XML API key is essentially a bearer token — protect it like a password. See the Admin Guide for rotation guidance.
3.3 HTTP API plugin (httpapi connection)
The collection ships an httpapi plugin at plugins/httpapi/panos.py named
paloaltonetworks.panos.panos. With it, you can drive PAN-OS as if it were
any other network device:
# inventory.yml
all:
hosts:
fw01:
ansible_host: 192.0.2.1
ansible_user: admin
ansible_password: "{{ vault_panos_pw }}"
ansible_connection: httpapi
ansible_network_os: paloaltonetworks.panos.panos
ansible_httpapi_use_ssl: true
ansible_httpapi_validate_certs: false
# Or use API key instead:
# ansible_api_key: "{{ vault_panos_apikey }}"
The plugin handles keygen, op, commit, and commit_all directly. Most
collection modules use the SDK path (provider dict) — the httpapi plugin is
useful for low-level XML interactions and for AAP-native workflows.
Common Panorama context parameters
When pointing a module at Panorama, you usually need to tell it where in the Panorama config tree to act:
| Parameter | Meaning | Default |
|---|---|---|
device_group |
Panorama device-group scope | shared |
template |
Panorama template scope (network config) | (none) |
template_stack |
Panorama template-stack scope | (none) |
vsys |
Firewall vsys (for direct-to-firewall modules) | vsys1 (objects) / shared (services) |
rulebase |
Pre/post rulebase for policy modules | pre-rulebase (Panorama) / rulebase (firewall) |
Note
Issue #611 — if you target a parent device group while child device groups have firewalls in them,
panos_device_groupmay error. Move firewalls to the deepest device group you want to manage.
4. Core Usage
4.1 The connection model
Every module needs to know where to talk and how to authenticate.
The connection logic lives in plugins/module_utils/panos.py (function
get_pandevice_parent). It accepts either a provider dict or the deprecated
classic params, then it:
- Imports
pan-os-pythonand verifies the minimum version the module declared. - Calls
PanDevice.create_from_device(ip, user, pass, api_key, port)— this introspects the device, decides whether it is aFirewallorPanorama, and returns a typed SDK object. - Walks down the SDK tree to the right scope: vsys, device group, template,
rulebase. Modules declare which scope they need via
vsys=True,device_group=True,template=True,rulebase=True. - Hands the parent object back to the module's
helper.process(module)call which performs the create/read/update/delete.
You don't need to think about most of this — it just works as long as
provider is correct.
4.2 The standard task pattern
Most collection modules follow the same skeleton:
- name: <verb> <object>
paloaltonetworks.panos.panos_<resource>:
provider: "{{ provider }}"
# scope (Panorama only — firewalls leave these unset):
device_group: "Production"
template: "DC-Template"
# the resource itself:
name: "MyObject"
<field_1>: <value_1>
<field_2>: <value_2>
# state — default 'present':
state: present
States supported by the network-resource family of modules (declared in
paloaltonetworks.panos.fragments.network_resource_module_state):
| State | Behaviour |
|---|---|
present |
Create if missing, update fields you specified. Default. |
absent |
Delete if it exists. No-op if it does not. |
replaced |
Replace the entire object — fields you don't pass are removed. |
merged |
Merge what you pass with what's there (additive). |
deleted |
Same as absent but in network-resource terminology. |
gathered |
Read-only — return the current config. Pair with gathered_filter. |
The simpler legacy modules accept only state: present and state: absent.
4.3 Idempotency
The collection is designed to be re-runnable. A task reports
changed=true only when it actually altered the firewall. The integration
tests in tests/integration/firewall/ verify this — every test creates an
object, then re-creates it and asserts result is not changed. Example
from tests/integration/firewall/test_panos_address_object.yml:
- name: test_panos_address_object - Create
paloaltonetworks.panos.panos_address_object:
provider: '{{ device }}'
name: 'Test-One'
value: '1.1.1.1'
register: result
- name: test_panos_address_object - Assert create was successful
assert:
that:
- result is success
- result is changed
- name: test_panos_address_object - Create (idempotence)
paloaltonetworks.panos.panos_address_object:
provider: '{{ device }}'
name: 'Test-One'
value: '1.1.1.1'
register: result
- name: test_panos_address_object - Assert create (idempotence) was successful
assert:
that:
- result is success
- result is not changed
4.4 Commit handling
PAN-OS has a candidate config and a running config. The modules write to the candidate. To make changes active, you must commit. The collection has three commit modules:
| Module | Target | Use case |
|---|---|---|
panos_commit_firewall |
A firewall (NGFW) | After firewall-direct config tasks |
panos_commit_panorama |
Panorama itself | After Panorama config tasks |
panos_commit_push |
Panorama → managed devices | Push device-group / template changes to firewalls |
The legacy panos_commit is deprecated in 4.0.0 in favour of the three
above (per meta/runtime.yml).
Pattern: do all your config changes, then commit at the end of the play.
- name: Commit firewall changes
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
description: "Daily automation commit"
sync: true
For Panorama → device push:
- name: Commit Panorama
paloaltonetworks.panos.panos_commit_panorama:
provider: "{{ panorama_provider }}"
device_groups: ['Production', 'DMZ']
sync: true
- name: Push to managed devices
paloaltonetworks.panos.panos_commit_push:
provider: "{{ panorama_provider }}"
style: 'device group'
name: 'Production'
sync: true
Warning
Issue #642 —
panos_security_rulemay automatically commit any other pending changes that were already in the candidate config. If you share a Panorama with humans, run a Panorama lock or check for pending changes before your run.
4.5 Error handling
Every module raises module.fail_json() with a structured msg. Wrap risky
tasks in block/rescue/always:
- name: Apply config with rollback
block:
- name: Add new rule
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
rule_name: "Allow-DNS"
source_zone: ['internal']
destination_zone: ['external']
application: ['dns']
action: 'allow'
rescue:
- name: Notify on failure
ansible.builtin.debug:
msg: "Rule creation failed — see prior task error"
always:
- name: Commit only if we got this far
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
when: not ansible_failed_task is defined
Common error families:
- PAN-OS API errors — surfaced as
PanOSAPIErrorwith the API code (Bad Xpath, Object not present, etc.). The full code map is inplugins/httpapi/panos.py. - Missing library errors —
pan-os-pythonorpan-pythonnot installed on the control node. - Min-version errors —
pan-os-python version (X) < minimum version (Y)when the module needs a newer SDK. - Connection timeouts —
Timed out waiting for job id <N>when a long op (commit, software install) blew throughpoll_timeout.
4.6 Roles pattern
The collection itself does not ship Ansible roles, but the canonical
deployment pattern is to wrap a set of related tasks into a role under
roles/. A typical layout:
roles/
├── pan_baseline/
│ ├── defaults/main.yml # default policy templates
│ ├── tasks/
│ │ ├── main.yml # entry point
│ │ ├── objects.yml # address, service, tag objects
│ │ ├── policy.yml # security + NAT rules
│ │ └── commit.yml
│ └── vars/main.yml
└── pan_panorama/
├── tasks/
│ ├── main.yml
│ ├── device_groups.yml
│ └── templates.yml
└── ...
Roles compose: a top-level playbook calls pan_baseline against firewalls
and pan_panorama against your Panorama hosts.
5. Common Tasks
Every recipe follows the same shape: Goal · Prerequisites · Steps · Verify · If it fails.
5.1 Add an address object across all device groups
Goal. Create the address object Web-CDN-Origin (1.2.3.4) in every device
group on Panorama, in a loop.
Prerequisites. Panorama reachable; device_groups variable defined
(can be discovered with panos_facts against Panorama).
Steps.
---
- name: Add address object across all device groups
hosts: panorama
connection: local
gather_facts: false
vars:
provider:
ip_address: "panorama.example.com"
username: "ansible"
password: "{{ vault_panos_pw }}"
device_groups: ['Production', 'DMZ', 'GuestNet', 'Lab']
tasks:
- name: Create Web-CDN-Origin in {{ item }}
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
device_group: "{{ item }}"
name: "Web-CDN-Origin"
value: "1.2.3.4"
address_type: "ip-netmask"
description: "CDN origin for marketing"
tag: ['Prod', 'CDN']
state: present
loop: "{{ device_groups }}"
- name: Commit Panorama
paloaltonetworks.panos.panos_commit_panorama:
provider: "{{ provider }}"
sync: true
Verify.
ansible panorama -m paloaltonetworks.panos.panos_object_facts \
-a "provider={{ provider }} object_type=address device_group=Production name=Web-CDN-Origin"
If it fails. Most common: a device group does not exist. Catch with
ignore_errors: true and feed the loop from panos_facts (subset
config) instead of a hard-coded list.
5.2 Bulk-create address objects from a CSV
Goal. Read addresses.csv (columns: name, value, type, description) and
create every row.
Prerequisites. CSV file in the playbook directory.
Steps.
---
- name: Bulk import addresses from CSV
hosts: localhost
connection: local
gather_facts: false
vars:
provider: "{{ panos_provider }}"
tasks:
- name: Read CSV
community.general.read_csv:
path: "addresses.csv"
delimiter: ","
register: csv_data
- name: Create each address object
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
name: "{{ item.name }}"
value: "{{ item.value }}"
address_type: "{{ item.type | default('ip-netmask') }}"
description: "{{ item.description | default(omit) }}"
state: present
loop: "{{ csv_data.list }}"
- name: Commit
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
Verify. ansible-playbook -v and watch every iteration — you want to
see changed: 1 per row on first run, ok: 1 per row on the second.
If it fails. The most common failure is address_type mismatch — for
example, a value 1.2.3.4-1.2.3.10 with address_type: ip-netmask. Run
with --check (check mode) once to surface schema issues without writing.
5.3 Add a security rule with logging and tagging
Goal. Allow internal users to reach 8.8.8.8/32 over DNS, log at session
end, tag the rule with Production, and place it before the existing rule
Default-Deny.
Prerequisites. Source/destination zones already exist; tag Production
already exists (or will be auto-created if you reference it as a string).
Steps.
- name: Allow DNS to public resolver
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
rule_name: "Allow-DNS-To-Public"
description: "Internal users → 8.8.8.8 DNS"
source_zone: ['internal']
destination_zone: ['external']
source_ip: ['any']
destination_ip: ['8.8.8.8']
application: ['dns']
service: ['application-default']
action: 'allow'
log_setting: 'PA-Logging'
log_end: true
tag_name: ['Production']
location: 'before'
existing_rule: 'Default-Deny'
state: present
- name: Commit
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
Verify. The panos_security_rule_facts module (or the new pattern,
panos_security_rule with state: gathered) returns the rule and its
position:
- name: Confirm rule exists
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
state: gathered
gathered_filter: "rule_name == 'Allow-DNS-To-Public'"
register: rules
- debug: var=rules
If it fails. existing_rule: 'Default-Deny' must match an existing rule
name exactly — typos cause Object not present. List rules first if unsure.
Tip
Use
audit_commentto leave a message in the rule's audit history —audit_comment: "ITSM-12345 — DNS exception"makes change-management post-mortems easier.
5.4 Schedule, commit and push from Panorama
Goal. After making policy changes on Panorama, commit Panorama itself
and then push the changes to Production and DMZ device groups.
Prerequisites. All changes already in the Panorama candidate config.
Steps.
- name: Commit Panorama configuration
paloaltonetworks.panos.panos_commit_panorama:
provider: "{{ panorama_provider }}"
description: "Quarterly hardening — ITSM-23456"
device_groups: ['Production', 'DMZ']
templates: ['DC-Template']
sync: true
register: pano_commit
- name: Push device-group config to managed firewalls
paloaltonetworks.panos.panos_commit_push:
provider: "{{ panorama_provider }}"
style: 'device group'
name: "{{ item }}"
description: "ITSM-23456 push"
sync: true
loop:
- 'Production'
- 'DMZ'
Verify. Check the returned jobid per push and panos_op to confirm
each managed firewall completed:
- name: Confirm push completed
paloaltonetworks.panos.panos_op:
provider: "{{ panorama_provider }}"
cmd: "show jobs all"
If it fails. Some devices in the group may be down; the push partial-fails
per device. Re-run targeting only the failing serials with devices: [<serial>].
5.5 Manage admin users (add / remove / rotate)
Goal. Create read-only admin audit-bot, rotate password for the existing
admin automation, and remove the disused admin legacy-admin.
Prerequisites. Connecting as a superuser admin.
Steps.
- name: Create read-only admin
paloaltonetworks.panos.panos_administrator:
provider: "{{ provider }}"
admin_username: 'audit-bot'
admin_password: "{{ vault_audit_bot_pw }}"
superuser_read_only: true
state: present
- name: Rotate password on automation admin
paloaltonetworks.panos.panos_administrator:
provider: "{{ provider }}"
admin_username: 'automation'
admin_password: "{{ vault_new_automation_pw }}"
superuser: true
state: present
no_log: true
- name: Remove legacy admin
paloaltonetworks.panos.panos_administrator:
provider: "{{ provider }}"
admin_username: 'legacy-admin'
state: absent
- name: Commit
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
Verify. SSH to the firewall and run show admins all, or use
panos_op with cmd: 'show admins all'.
If it fails. PAN-OS hashes admin passwords with request password-hash
on each call, so even unchanged passwords look like changes (changed=True
every run). This is documented behaviour. Use --check if that bothers your
audit logs.
Caution
Never put
admin_passwordin plaintext in your playbook. Always wrap it inansible-vault encrypt_stringand reference the vault var. Thepanos_administratormodule setsno_log: Trueon the password parameter, but only for the parameter logging — your YAML file is still readable to anyone with shell access.
5.6 Configure SNMP / syslog / LDAP server profiles (downstream creds)
Goal. Ship a syslog profile to Splunk and an SNMPv3 server profile to the NMS.
Prerequisites. None — these are config items, no external system needs to be reachable from the firewall during the task.
Steps.
- name: Define syslog server profile
paloaltonetworks.panos.panos_syslog_profile:
provider: "{{ provider }}"
name: "Splunk"
state: present
- name: Add Splunk syslog server entry
paloaltonetworks.panos.panos_syslog_server:
provider: "{{ provider }}"
syslog_profile: "Splunk"
name: "splunk-prod"
server: "splunk.example.com"
transport: "TCP"
port: 514
format: "BSD"
facility: "LOG_USER"
state: present
- name: Define SNMPv3 server profile
paloaltonetworks.panos.panos_snmp_profile:
provider: "{{ provider }}"
name: "NMS-SNMPv3"
state: present
- name: Add SNMPv3 server entry
paloaltonetworks.panos.panos_snmp_v3_server:
provider: "{{ provider }}"
snmp_profile: "NMS-SNMPv3"
name: "nms-prod"
manager: "10.0.50.10"
user: "panos-snmp"
auth_password: "{{ vault_snmp_auth }}"
priv_password: "{{ vault_snmp_priv }}"
state: present
no_log: true
Verify. panos_op with cmd: 'show log forwarding profile <name>' and
show snmp-server.
If it fails. A profile name typo causes panos_syslog_server to fail
because the parent panos_syslog_profile did not exist. Ensure the profile
task runs first.
Important
Treat all profile passwords as secret material. Always use
no_log: trueon the task andansible-vault encrypt_stringon the value.
5.7 Apply IronSkillet-style hardening via Ansible
Goal. Apply a baseline of best-practice config — admins, log forwarding, strict security profiles, default-deny rule.
Prerequisites. Greenfield firewall, fresh boot.
Steps. Build a role that wraps multiple modules.
# roles/pan_hardening/tasks/main.yml
- import_tasks: admins.yml
- import_tasks: log_forwarding.yml
- import_tasks: security_profiles.yml
- import_tasks: default_deny.yml
- name: Final commit
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
description: "Initial hardening apply"
# roles/pan_hardening/tasks/security_profiles.yml
- name: Strict security profile group
paloaltonetworks.panos.panos_pg:
provider: "{{ provider }}"
pg_name: "Strict"
virus: "default"
spyware: "strict"
vulnerability: "strict"
url_filtering: "default"
file_blocking: "strict"
wildfire: "default"
state: present
# roles/pan_hardening/tasks/default_deny.yml
- name: Default-deny intrazone
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
rule_name: "Default-Deny-All"
source_zone: ['any']
destination_zone: ['any']
source_ip: ['any']
destination_ip: ['any']
application: ['any']
service: ['any']
action: 'deny'
log_end: true
rule_type: 'universal'
location: 'bottom'
state: present
Verify. Run panos_security_rule_facts and confirm Default-Deny-All
exists and is at the bottom.
If it fails. PAN-OS 10.0+ removed the hip_profiles field; the
panos_security_rule module handles this for you (see Helper.spec_handling
in plugins/modules/panos_security_rule.py). If you see a HIP-related error,
upgrade the collection.
5.8 Backup configurations to disk
Goal. Pull the running config from every firewall and write it to a local file with the firewall's hostname and timestamp.
Prerequisites. Local directory writable by the playbook.
Steps.
- name: Export running configuration
paloaltonetworks.panos.panos_export:
provider: "{{ provider }}"
category: "configuration"
filename: "/var/backups/panos/{{ inventory_hostname }}-{{ ansible_date_time.iso8601 }}.xml"
create_directory: true
- name: Export tech support bundle (optional, for incident snapshots)
paloaltonetworks.panos.panos_export:
provider: "{{ provider }}"
category: "tech-support"
filename: "/var/backups/panos/{{ inventory_hostname }}-tech-support.tgz"
timeout: 1200
Verify. xmllint --noout /var/backups/panos/<file>.xml should parse
without error. The file's root element is <config>.
If it fails. Tech-support exports take minutes; bump the timeout
parameter (default 600 sec).
Caution
The exported XML config is sensitive: it can include admin password hashes, downstream service credentials and certificates. Restrict ACLs on
/var/backups/panos, encrypt at rest, and handle the file like other credential material. See the Admin Guide §7.4.
5.9 Get system info / interfaces / sessions for a monitoring playbook
Goal. Build a daily inventory snapshot — hostname, model, version, interfaces, session counts.
Prerequisites. None.
Steps.
- name: Collect facts
paloaltonetworks.panos.panos_facts:
provider: "{{ provider }}"
gather_subset: ['system', 'interfaces', 'session', 'ha']
register: facts
- name: Persist to file
ansible.builtin.copy:
content: "{{ facts | to_nice_yaml }}"
dest: "/var/log/panos-inventory/{{ inventory_hostname }}.yml"
The keys exposed by panos_facts follow Ansible network convention —
ansible_net_hostname, ansible_net_version, ansible_net_interfaces,
ansible_net_session_usage, ansible_net_ha_localmode, etc. (See
plugins/modules/panos_facts.py RETURN block.)
Verify. Open the file — you should see structured YAML with the host's metadata.
If it fails. Panorama only supports the system, ha, and config
subsets. Asking for interfaces against Panorama returns nothing for that
subset.
5.10 Manage HA pairs
Goal. Bring up an Active/Passive HA pair on two firewalls.
Prerequisites. Both firewalls have ethernet1/5 available; they can
reach each other on the HA1/HA2 networks.
Steps.
- name: Set HA ports on both firewalls
paloaltonetworks.panos.panos_interface:
provider: "{{ provider }}"
if_name: "{{ item }}"
mode: "ha"
enable_dhcp: false
loop:
- "ethernet1/5"
- "ethernet1/6"
- name: Configure Active/Passive HA
paloaltonetworks.panos.panos_ha:
provider: "{{ provider }}"
state: present
ha_peer_ip: "192.168.50.{{ 2 if (inventory_hostname == 'fw01') else 1 }}"
ha_mode: "active-passive"
ha1_ip_address: "192.168.50.{{ 1 if (inventory_hostname == 'fw01') else 2 }}"
ha1_netmask: "255.255.255.0"
ha1_port: "ethernet1/5"
ha2_ip_address: "192.168.51.{{ 1 if (inventory_hostname == 'fw01') else 2 }}"
ha2_netmask: "255.255.255.0"
ha2_port: "ethernet1/6"
ha_state_sync: true
- name: Confirm HA active node
paloaltonetworks.panos.panos_active_in_ha:
provider: "{{ provider }}"
register: active_ha
Verify. panos_active_in_ha returns response.active = true on the
primary, false on the passive node. panos_op with cmd: 'show high-availability state' shows the full state.
If it fails. HA1 link must come up before the HA service starts. If the peers cannot see each other, both firewalls report Initial state. Check firewall-to-firewall reachability.
5.11 Configure a virtual router and static route
Goal. Create a virtual router VR-Edge and add a default route through
10.0.0.1.
Prerequisites. Interface ethernet1/1 exists.
Steps.
- name: Create virtual router
paloaltonetworks.panos.panos_virtual_router:
provider: "{{ provider }}"
name: "VR-Edge"
interface: ['ethernet1/1']
state: present
- name: Add default route
paloaltonetworks.panos.panos_static_route:
provider: "{{ provider }}"
virtual_router: "VR-Edge"
name: "default"
destination: "0.0.0.0/0"
nexthop_type: "ip-address"
nexthop: "10.0.0.1"
interface: "ethernet1/1"
metric: 10
state: present
- name: Commit
paloaltonetworks.panos.panos_commit_firewall:
provider: "{{ provider }}"
Verify. panos_op with cmd: 'show routing route' lists the new route.
If it fails. Issue #660
— panos_logical_router_vrf_static_route does not yet support IPv6 next-hop.
For IPv6 default routes today, fall back to panos_op with the raw XML
configuration command.
5.12 Configure a NAT rule
Goal. SNAT outbound traffic from Trust-L3 zone to Untrusted-L3 via
the egress interface (ethernet1/1), using interface address.
Prerequisites. Zones and interface already configured.
Steps.
- name: Outbound SNAT rule
paloaltonetworks.panos.panos_nat_rule2:
provider: "{{ provider }}"
name: "Outbound-SNAT"
description: "Internet egress SNAT"
nat_type: 'ipv4'
from_zones: ['Trust-L3']
to_zones: ['Untrusted-L3']
to_interface: 'ethernet1/1'
service: 'any'
source_addresses: ['any']
destination_addresses: ['any']
source_translation_type: 'dynamic-ip-and-port'
source_translation_address_type: 'interface-address'
source_translation_interface: 'ethernet1/1'
state: present
For destination NAT (publishing a server):
- name: DNAT to web server
paloaltonetworks.panos.panos_nat_rule2:
provider: "{{ provider }}"
name: "DNAT-Web-A"
nat_type: 'ipv4'
from_zones: ['Untrusted-L3']
to_zones: ['Untrusted-L3']
source_addresses: ['any']
destination_addresses: ['203.0.113.10']
destination_translated_address: '10.0.10.20'
destination_translated_port: 443
state: present
Verify. panos_op with cmd: 'show running nat-rule-base' enumerates
NAT rules.
If it fails. panos_nat_rule2 is the current module. The legacy
panos_nat_rule (without the 2) is deprecated in 4.0.0 per
meta/runtime.yml — it redirects to panos_nat_rule2 automatically, but
new playbooks should use the 2 variant directly.
5.13 Run an arbitrary operational command (panos_op)
Goal. Execute show clock, show counter global, or any other op
command, and parse the result.
Steps.
- name: Show counter global
paloaltonetworks.panos.panos_op:
provider: "{{ provider }}"
cmd: 'show counter global'
register: counters
- debug:
msg: "{{ counters.stdout | from_json }}"
XML form when you need a structured op:
- name: Show running config (XML form)
paloaltonetworks.panos.panos_op:
provider: "{{ provider }}"
cmd: '<show><config><running></running></config></show>'
cmd_is_xml: true
Verify. Both stdout (JSON-encoded) and stdout_xml are returned.
If it fails. Issue #637
notes that panos_op does not support multi-line CLI commands — wrap each
command in its own task.
5.14 Software upgrade with readiness checks
Goal. Pre-download PAN-OS 11.1.4 to the firewall, run readiness checks, then install with restart.
Steps.
- name: Pre-download PAN-OS 11.1.4
paloaltonetworks.panos.panos_software:
provider: "{{ provider }}"
version: "11.1.4"
install: false
restart: false
sync_to_peer: true
- name: Pre-upgrade readiness checks
paloaltonetworks.panos.panos_readiness_checks:
provider: "{{ provider }}"
checks:
- 'panorama'
- 'ntp_sync'
- 'free_disk_space'
- 'ha'
- name: Snapshot pre-upgrade state
paloaltonetworks.panos.panos_state_snapshot:
provider: "{{ provider }}"
state_areas: ['arp_table', 'route', 'session_stats']
file_path: "/tmp/preupgrade-{{ inventory_hostname }}.json"
- name: Install and reboot
paloaltonetworks.panos.panos_software:
provider: "{{ provider }}"
version: "11.1.4"
install: true
restart: true
timeout: 1800
- name: Wait for firewall to come back
paloaltonetworks.panos.panos_check:
provider: "{{ provider }}"
initial_delay: 120
interval: 15
timeout: 1200
Verify. panos_facts with gather_subset: ['system'] returns
ansible_net_version: 11.1.4.
If it fails. Issue #658
— in some scenarios panos_software hangs after install. If you see this,
break out the steps (install: false then a manual op-command install via
panos_op). Issue #662
warns that panos_software looks for a x.x.0 base before installing a
patch version — make sure the base image is downloaded first.
6. Troubleshooting
Important
Always run with
-vvvonce when something breaks. The verbose output includes the XML request and response, which makes XML-API errors much easier to diagnose.
Diagnostic decision tree
- Module errors before any HTTP request → It's a Python/SDK problem
(
pan-os-pythonmissing or wrong version). See "Missing required library" below. - Module errors with
Failed connection→ Network/auth. Check provider IP, credentials, and that 443/tcp is reachable. - Module errors with a PAN-OS API code → It's a config-validation problem. Look up the API code below.
- Module looks like it succeeded but firewall didn't change → You forgot to commit.
- Tasks pass but downstream firewalls do not see the change → Panorama
committed, but you didn't push (
panos_commit_push).
Error: Missing required library "pan-os-python"
Cause. pan-os-python is not installed in the Python interpreter that
Ansible used to run the module. This is the Python interpreter set by
ansible_python_interpreter (or auto-detected).
Solution.
ansible -m setup localhost | grep ansible_python_version— what version was selected?- Find the interpreter path:
ansible -m setup localhost | grep ansible_python.executable. - Install the SDK there:
<that-path> -m pip install pan-os-python panos-upgrade-assurance. - Re-run.
Prevention. Pin ansible_python_interpreter in inventory to a known
virtualenv that has the SDK.
Error: pan-os-python version (X.Y.Z) < minimum version (A.B.C)
Cause. Module declared min_pandevice_version=(A,B,C) — the SDK is too
old. Source: plugins/module_utils/panos.py, _MIN_VERSION_ERROR.
Solution. pip install --upgrade 'pan-os-python<2.0'.
Prevention. Treat the SDK as a managed dependency — pin a recent
known-good version in your requirements.txt.
Error: Failed connection: <pan-os-python message>
Cause. TCP/443 is unreachable, the credentials are wrong, or TLS verification failed.
Solution.
curl -kv https://<firewall>/api/?type=keygen&user=admin&password=...— does keygen succeed?- If keygen works but Ansible fails, check
portin your provider — must be443(default) or whatever the firewall is actually using. - If TLS fails, the firewall has a self-signed cert. Either trust it on the
control node or use the
httpapiplugin withansible_httpapi_validate_certs: false.
Error: Bad Xpath
Cause. A reference to an object/scope that does not exist (typically a non-existent vsys, device group, or template).
Solution. Verify with panos_facts (subset config) that the scope you
named exists. Common case: targeting device_group: 'Production' when the
DG is actually called production (case-sensitive).
Error: Object not present
Cause. Trying to reference an object that has not been created. Usual case: rule references an address object you forgot to add first.
Solution. Always create dependent objects before the policy that uses them.
Error: Reference count not zero
Cause. Trying to delete an object that is still referenced (e.g., an address object used by a rule).
Solution. Delete the references first (rule → object), then the object itself.
Error: Timed out waiting for job id <N>
Cause. The op (commit, software install, tech-support export) took longer than the module's timeout.
Solution. Bump timeout on the module — panos_software defaults to
1200 sec, panos_export to 600 sec. The XML API job-poll interval is also
configurable on panos_op (poll_interval, poll_timeout).
Error: Data too large for XML API request
Cause. Source: plugins/httpapi/panos.py:578 — the request body
exceeded 5 MB. Mostly hits when feeding huge XML chunks via
panos_config_element or panos_op.
Solution. Split the operation into smaller chunks. For bulk imports, use the per-object modules in a loop instead of one giant XML element.
Error: Invalid response from API
Cause. The firewall returned an HTTP code other than 200/400/403 (often a 500 from PAN-OS).
Solution. Re-run with -vvv and inspect the raw response. If it's a
500, file a PAN-OS support case — this is firewall-side.
Error: auto-commit job has not finished
Cause. The firewall is still booting. The auto-commit job runs at boot and must finish before the API will accept new commits.
Solution. Wrap pre-config tasks in a panos_check task with
initial_delay: 60 and timeout: 600.
Issue: Idempotency reports changed every run
Cause for admin password. PAN-OS hashes the password with a salt every
time, so the hash differs even when the password is the same. The
panos_administrator module documents this — changed=True every run when
admin_password is set is expected.
Cause for any other module. Typically a default value mismatch (you omitted a field; the SDK reads it as the default; on the next run you don't match). Pin the field explicitly.
Issue: panos_software keeps trying to download
Cause. Issue #662
— the module looks for a x.x.0 base image before installing a patch like
11.1.4.
Solution. First task: panos_software with the base version
(11.1.0) and install: false. Second task: install your patch version.
Issue: panos_security_rule commits other people's changes
Cause. Issue #642.
Solution. Use a Panorama config lock during your run; or check pending
changes with panos_op (cmd: 'show config pending changed-by') and
fail-fast if there are unrelated pending commits.
Issue: panos_export doesn't pretty-print XML
Cause. Issue #636. PAN-OS exports the running config as a single line.
Solution. Pipe through xmllint --format after the task completes.
7. FAQ
Q: Where do I find the official module index?
The full per-module reference is on
pan.dev and on
Ansible Galaxy.
Both are generated from the same DOCUMENTATION strings that live in
plugins/modules/*.py, so the in-tree source is the canonical version.
Q: Which Ansible version do I need?
>=2.16.0 (declared in meta/runtime.yml). Ansible Core 2.16, 2.17, 2.18,
2.19 and 2.20 are all in the supported sanity matrix
(tests/sanity/ignore-2.16.txt … ignore-2.20.txt).
Q: What is the minimum Python version?
3.10 (declared in requirements.txt via the python_version >= "3.10"
markers).
Q: Does the collection talk to firewalls over SSH?
No. The collection talks to the PAN-OS XML API over HTTPS (default
port 443). Ansible itself runs on the control node with connection: local.
The firewall is contacted via HTTP from the control node.
Q: What's the difference between panos_commit, panos_commit_firewall, panos_commit_panorama, and panos_commit_push?
panos_commit— legacy, generic. Deprecated in 4.0.0 permeta/runtime.yml; redirects topanos_commit_firewall.panos_commit_firewall— commit candidate config on a firewall (NGFW).panos_commit_panorama— commit candidate config on Panorama itself.panos_commit_push— push committed Panorama config to managed firewalls.
For new playbooks, always use the three specific commit modules.
Q: What's the difference between panos_nat_rule and panos_nat_rule2?
panos_nat_rule2 is the current network-resource-style module. The legacy
panos_nat_rule is deprecated in 4.0.0 and redirects to
panos_nat_rule2. Use panos_nat_rule2 directly in new code.
Q: How do I list every existing rule, object, or interface?
Use the new state: gathered pattern on the relevant module:
- name: List all security rules
paloaltonetworks.panos.panos_security_rule:
provider: "{{ provider }}"
state: gathered
register: rules
The deprecated *_facts modules (panos_security_rule_facts,
panos_zone_facts, panos_virtual_router_facts) all redirect to the
modern modules with state: gathered (per meta/runtime.yml).
Q: Can I use the collection from Ansible Automation Platform (AAP)?
Yes. Build an Execution Environment that contains the collection plus the Python deps. See Section 2, Method D.
Q: How do I handle multiple Panorama device groups?
Loop over the device_group parameter:
- name: Apply object to every DG
paloaltonetworks.panos.panos_address_object:
provider: "{{ provider }}"
device_group: "{{ item }}"
name: "Web-CDN-Origin"
value: "1.2.3.4"
loop: ['Production', 'DMZ', 'Lab']
Q: Does the collection support PAN-OS 8.0 / 9.0 / 10.x / 11.x?
The collection is compatible across PAN-OS 8.x onwards, with per-module
minimum-version checks. Some newer modules require PAN-OS 10.x or 11.x —
each module declares this with min_panos_version in its
get_connection() call.
Q: What does state: replaced actually do?
It replaces the entire object — fields you don't set are removed (set to
default or null). Use it when you want a known-clean state. Contrast
state: merged, which only adds/changes fields you specify.
Q: How do I use the gathered_filter parameter?
gathered_filter is a string expression to narrow state: gathered
results. Examples: name == 'Web-Server-A', tag contains 'Production'.
Reference: the paloaltonetworks.panos.fragments.gathered_filter doc
fragment.
Q: Where are the integration tests?
Under tests/integration/firewall/ and tests/integration/panorama/.
They run against real firewalls/Panorama and assert idempotency. The
README at tests/integration/README.md describes the inventory and
firewall preconditions.
Q: Can I write my own module on top of this collection?
Yes. Subclass ConnectionHelper from
plugins/module_utils/panos.py and reuse get_connection() /
get_pandevice_parent(). The Admin Guide §8 covers this.
Q: Does the collection support Event-Driven Ansible?
Yes. The extensions/eda/plugins/event_source/logs.py plugin exposes a
syslog/EDA event source for PAN-OS logs. There is also a sample rulebook
at extensions/eda/rulebooks/decryption.yaml.
Q: How do I rotate the API key?
Either regenerate via the firewall UI / panos_api_key module, or use
PAN-OS API key revocation (debug api-key rotate). Update your
provider.api_key everywhere afterwards. See Admin Guide §7 for full
rotation guidance after backups.
Q: I see deprecation warnings. Should I act on them?
Yes — meta/runtime.yml schedules these removals for 4.0.0. The
deprecation list is:
| Old module | Replacement |
|---|---|
panos_admin |
panos_administrator |
panos_commit |
panos_commit_firewall / panos_commit_panorama / panos_commit_push |
panos_dag |
panos_address_group |
panos_dag_tags |
panos_registered_ip |
panos_object |
panos_address_object |
panos_object_facts |
panos_address_object (state: gathered) |
panos_nat_rule |
panos_nat_rule2 |
panos_nat_rule_facts |
panos_nat_rule2 (state: gathered) |
panos_query_rules |
panos_match_rule |
panos_sag |
panos_address_group |
panos_security_rule_facts |
panos_security_rule (state: gathered) |
panos_virtual_router_facts |
panos_virtual_router (state: gathered) |
panos_zone_facts |
panos_zone (state: gathered) |