# pan-os-ansible User Guide

> [!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

```bash
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:

```yaml
---
- 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:

```bash
PANOS_PASSWORD='your-pan-password' ansible-playbook firewall-hello.yml
```

Expected output (abridged):

```text
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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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.

#### Method A — Ansible Galaxy (recommended)

```bash
ansible-galaxy collection install paloaltonetworks.panos
pip install -r ~/.ansible/collections/ansible_collections/paloaltonetworks/panos/requirements.txt
```

Pin a specific collection version:

```bash
ansible-galaxy collection install paloaltonetworks.panos:==3.3.0
```

#### Method B — `requirements.yml` for project-local installs

```yaml
# requirements.yml
collections:
  - name: paloaltonetworks.panos
    version: ">=3.0.0,<4.0.0"
```

```bash
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

```bash
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`:

```yaml
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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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

```bash
ansible-galaxy collection list paloaltonetworks.panos
```

```text
# /home/ansible/.ansible/collections/ansible_collections
Collection                Version
------------------------- -------
paloaltonetworks.panos    3.3.0
```

Confirm Python deps:

```bash
python3 -c "import panos; print(panos.__version__)"
```

```text
1.12.4
```

Smoke-test against a firewall:

```bash
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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/648) and [#655](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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. |

#### 3.1 Provider dict (recommended)

Every module accepts a `provider` parameter — a dict of connection details:

```yaml
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:

```yaml
- 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:

```yaml
- 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:

```yaml
# 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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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:

```yaml
- 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`:

```yaml
- 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.

```yaml
- name: Commit firewall changes
  paloaltonetworks.panos.panos_commit_firewall:
    provider: "{{ provider }}"
    description: "Daily automation commit"
    sync: true
```

For Panorama → device push:

```yaml
- 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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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`:

```yaml
- 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:

```text
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.**

```yaml
---
- 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.**

```bash
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.**

```yaml
---
- 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.**

```yaml
- 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:

```yaml
- 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.**

```yaml
- 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:

```yaml
- 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.**

```yaml
- 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.**

```yaml
- 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.

```yaml
# 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"
```

```yaml
# 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
```

```yaml
# 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.**

```yaml
- 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.**

```yaml
- 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.**

```yaml
- 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.**

```yaml
- 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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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.**

```yaml
- 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):

```yaml
- 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.**

```yaml
- 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:

```yaml
- 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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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.**

```yaml
- 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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/586),
[#568](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/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](https://pan.dev/ansible/docs/panos) and on
[Ansible Galaxy](https://galaxy.ansible.com/ui/repo/published/paloaltonetworks/panos/docs).
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:

```yaml
- 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:

```yaml
- 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`) |

---

_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._
