pan-os-ansible User Guide

Generated April 2026 from PaloAltoNetworks/pan-os-ansible at 0ea91f8

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Unofficial, AI-generated guide for a Palo Alto Networks open-source repository; not reviewed by Palo Alto Networks.

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 commit 0ea91f8 (collection version 3.3.0) and was written from the in-tree module sources in plugins/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: deleted on the network-resource modules.
  • Single connection model via the provider dict (IP + credentials), reusable across every task.
  • First-class Panorama support: device_group, template, template_stack, vsys parameters on every applicable module.
  • Built-in HTTP API plugin (paloaltonetworks.panos.panos) for inventory-based connection if you prefer the httpapi connection style over provider.
  • Shipping event_source plugin 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.txt via the python_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 by panos_active_in_ha, panos_readiness_checks, panos_snapshot_report, and panos_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 ok instead of changed because panos_address_object reads the firewall, sees the object already exists with the same value, and does nothing. The same is true for every module in the state: present family.

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 after pip 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 with ansible -m setup localhost | grep ansible_python_version and pin ansible_python_interpreter accordingly.

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.

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. If ansible-builder build fails on dependency resolution, run with --prune-images and bump base image to a recent awx-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 with pip 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 with pip install --upgrade 'pan-os-python<2.0'. The error path is plugins/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 set collections: in your play.

Note

Issue #648 and #655 note that requirements.txt pins 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.

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 provider in group_vars/firewalls.yml so every host inherits it. Override per-host in host_vars/<hostname>.yml only 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_key is set, username and password are 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_group may 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:

  1. Imports pan-os-python and verifies the minimum version the module declared.
  2. Calls PanDevice.create_from_device(ip, user, pass, api_key, port) — this introspects the device, decides whether it is a Firewall or Panorama, and returns a typed SDK object.
  3. 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.
  4. 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_rule may 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 PanOSAPIError with the API code (Bad Xpath, Object not present, etc.). The full code map is in plugins/httpapi/panos.py.
  • Missing library errors — pan-os-python or pan-python not 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 through poll_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_comment to 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_password in plaintext in your playbook. Always wrap it in ansible-vault encrypt_string and reference the vault var. The panos_administrator module sets no_log: True on 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: true on the task and ansible-vault encrypt_string on 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 -vvv once when something breaks. The verbose output includes the XML request and response, which makes XML-API errors much easier to diagnose.

Diagnostic decision tree

  1. Module errors before any HTTP request → It's a Python/SDK problem (pan-os-python missing or wrong version). See "Missing required library" below.
  2. Module errors with Failed connection → Network/auth. Check provider IP, credentials, and that 443/tcp is reachable.
  3. Module errors with a PAN-OS API code → It's a config-validation problem. Look up the API code below.
  4. Module looks like it succeeded but firewall didn't change → You forgot to commit.
  5. 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.

  1. ansible -m setup localhost | grep ansible_python_version — what version was selected?
  2. Find the interpreter path: ansible -m setup localhost | grep ansible_python.executable.
  3. Install the SDK there: <that-path> -m pip install pan-os-python panos-upgrade-assurance.
  4. Re-run.

Prevention. Pin ansible_python_interpreter in inventory to a known virtualenv that has the SDK.

(Issue #586, #568.)

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.

  1. curl -kv https://<firewall>/api/?type=keygen&user=admin&password=... — does keygen succeed?
  2. If keygen works but Ansible fails, check port in your provider — must be 443 (default) or whatever the firewall is actually using.
  3. If TLS fails, the firewall has a self-signed cert. Either trust it on the control node or use the httpapi plugin with ansible_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 per meta/runtime.yml; redirects to panos_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)