# pan-os-ansible Administrator & Developer Guide

> [!NOTE]
> This guide is the source-based administrator and developer reference for `paloaltonetworks.panos`, the official PAN-OS Ansible Collection. It is built from the in-tree sources at commit `0ea91f8` (collection version `3.3.0`) and covers architecture, configuration reference, scaling, operations, performance, security, internals, and integration. Pair it with the User Guide for usage and recipes.

The collection is a thin Ansible facade over the `pan-os-python` SDK and the
PAN-OS XML API. It contains **118 modules** in `plugins/modules/`, one
shared `ConnectionHelper` in `plugins/module_utils/panos.py`, an `httpapi`
plugin at `plugins/httpapi/panos.py`, and an Event-Driven Ansible (EDA)
event source under `extensions/eda/`. There is no on-firewall agent —
everything talks to PAN-OS over its XML API on TCP/443.

## 1. Architecture Overview

### Where the collection sits

```
+-------------------+        +----------------------+         +----------------+
| Ansible Control   |        | paloaltonetworks.    |         | pan-os-python  |
| Node (your CI,    |        | panos collection     |         | SDK (1.12.4+)  |
| AAP, or laptop)   |--------|  plugins/modules/    |---------|   PanDevice    |
|                   |        |  plugins/httpapi/    |         |   Firewall     |
| ansible-playbook  |        |  plugins/module_utils|         |   Panorama     |
| (Python 3.10+)    |        | (ConnectionHelper)   |         +----------------+
+-------------------+        +----------------------+                 |
                                                                      | XML over HTTPS
                                                                      v
                                                            +-------------------+
                                                            |  PAN-OS XML API   |
                                                            |  (TCP/443)        |
                                                            |   /api/?type=...  |
                                                            +-------------------+
                                                                      |
                                                                      v
                                                            +-------------------+
                                                            | NGFW / Panorama   |
                                                            | candidate config  |
                                                            | running config    |
                                                            +-------------------+
```

The control node runs `ansible-playbook`, which loads the
`paloaltonetworks.panos` collection and executes one of the 118 modules.
Each module imports `pan-os-python` (`import panos`) and uses
`ConnectionHelper.get_pandevice_parent()` to construct the
SDK object tree. The SDK then makes XML API calls to the firewall over
HTTPS. The firewall mutates its **candidate** configuration; nothing is
active on the firewall until a commit module is invoked.

### Component inventory

| Component | Path | Role |
|-----------|------|------|
| 118 task modules | `plugins/modules/panos_*.py` | One module per PAN-OS resource — the public surface |
| Shared helper | `plugins/module_utils/panos.py` | `ConnectionHelper`, `get_connection()`, `cmd_xml()`, idempotency engine |
| HTTP API plugin | `plugins/httpapi/panos.py` | Inventory-driven `httpapi` connection; raw XML API client |
| Doc fragments | `plugins/doc_fragments/fragments.py` | 16 reusable doc fragments (provider, vsys, device_group, state, …) |
| EDA event source | `extensions/eda/plugins/event_source/logs.py` | Stream PAN-OS logs to EDA rulebooks |
| Sample rulebook | `extensions/eda/rulebooks/decryption.yaml` | Reference EDA rulebook |
| Integration tests | `tests/integration/firewall/`, `tests/integration/panorama/` | Live-firewall idempotency tests |
| Unit tests | `tests/unit/httpapi/`, `tests/unit/plugins/module_utils/` | httpapi plugin and module_utils unit coverage |
| Sanity-test exclusions | `tests/sanity/ignore-2.16.txt` … `ignore-2.20.txt` | Per-Ansible-version sanity-test allowlists |
| Galaxy metadata | `galaxy.yml` | Collection name, version, dependencies, tags |
| Runtime routing | `meta/runtime.yml` | Required Ansible version, deprecation redirects |

### Technology stack

| Layer | Component | Version |
|-------|-----------|---------|
| Ansible Core | The runtime | `>=2.16.0` per `meta/runtime.yml` |
| Python | Control-node interpreter | `>=3.10` per `requirements.txt` |
| PAN-OS SDK | `pan-os-python` (`panos`) | `>=1.12.4,<2.0` |
| Upgrade Assurance | `panos-upgrade-assurance` | `>=2.0.0,<3.0` |
| XML/HTTP | `xmltodict`, `aiohttp`, `dpath` | Pinned in `requirements.txt` |
| Distribution | Galaxy + GitHub releases | Versioned via `semantic-release` (see `.releaserc.json`) |
| Test framework | `pytest`, `molecule`-style integration playbooks | In `tests/` |

> [!NOTE]
> The SDK rename history matters. `pandevice` was the original SDK name; it became `pan-os-python` (importable as `panos`) when 1.0 shipped. Every module in this collection has a fallback `try/except ImportError` chain that imports from the new name first and falls back to `pandevice` for backwards compat. New code should target `panos` only.

### Data flow for a single task

```
ansible-playbook task                 plugins/modules/panos_address_object.py
        |                                        |
        | invokes                                |
        v                                        v
+----------------------+              +------------------------+
| AnsibleModule init   |              | get_connection(...)    |
| with argument_spec   |------------->| -> ConnectionHelper    |
+----------------------+              +------------------------+
                                                |
                                                | get_pandevice_parent()
                                                v
                                      +------------------------+
                                      | PanDevice.create_      |
                                      | from_device(provider)  |
                                      | -> Firewall|Panorama   |
                                      +------------------------+
                                                |
                                                | helper.process(module)
                                                v
                                      +------------------------+
                                      | sdk_cls.refresh()      |
                                      | -> compare candidate vs|
                                      | -> apply_state()       |
                                      | -> set/edit/delete XML |
                                      +------------------------+
                                                |
                                                v
                                      module.exit_json(changed=...)
```

### Key invariants

- **Modules never commit by default.** Configuration changes always land in
  the candidate; you must run a commit module (`panos_commit_firewall`,
  `panos_commit_panorama`, or `panos_commit_push`) to make them active. The
  legacy `with_commit` flag on some older modules is deprecated for the
  4.0.0 major release.
- **One module per resource type.** Address objects → `panos_address_object`,
  security rules → `panos_security_rule`, NAT rules → `panos_nat_rule2`,
  and so on. The 1:1 mapping makes diff/idempotency cheap.
- **All state lives on the device.** No collection-side cache or DB. Every
  task fetches the current candidate state on demand and computes the diff.
- **The XML API is the source of truth for capability.** A module's
  parameters are the union of what the SDK exposes and what the underlying
  PAN-OS XML schema accepts.

> [!NOTE]
> Despite 118 modules, the design is non-obvious for one reason: every module is essentially a thin descriptor that hands off to `ConnectionHelper.process()` in `plugins/module_utils/panos.py`. That single 1900-line file handles connection, version checks, scope walking, idempotency, diff, gathered-filter, and XML serialisation. Read that file once and you have read 80 % of the collection.

---

## 2. System Requirements

### Control node (where `ansible-playbook` runs)

| Item | Requirement |
|------|-------------|
| OS | Any OS that runs Ansible Core 2.16+ (Linux, macOS, WSL2) |
| RAM | 1 GB minimum; 4 GB recommended for parallel forks |
| Disk | 200 MB for the collection + SDK |
| Python | 3.10, 3.11, 3.12, or 3.13 |
| Ansible Core | 2.16 → 2.20 (sanity-tested per `tests/sanity/ignore-2.*.txt`) |
| Network egress | TCP/443 to firewalls and Panorama (no inbound required) |

### Python dependencies

From `requirements.txt`:

```text
aiohttp>=3.8.4 ; python_version >= "3.10" and python_version < "4.0"
dpath>=2.1.5 ; python_version >= "3.10" and python_version < "4.0"
pan-os-python>=1.12.4,<2.0 ; python_version >= "3.10" and python_version < "4.0"
panos-upgrade-assurance>=2.0.0,<3.0 ; python_version >= "3.10" and python_version < "4.0"
xmltodict>=0.12.0 ; python_version >= "3.10" and python_version < "4.0"
```

> [!IMPORTANT]
> Issues [#648](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/648), [#655](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/655) — the upper bounds in `requirements.txt` are tighter than strictly necessary on some non-PANW deps. If you hit a pip resolver conflict, you can usually relax `aiohttp`, `dpath`, or `xmltodict` upper bounds. **Do not** relax `pan-os-python<2.0` — the collection uses 1.x APIs.

### PAN-OS device

| Item | Requirement |
|------|-------------|
| PAN-OS version | 8.0+ (per-module checks declare more recent minimums) |
| Management plane | Reachable on TCP/443 from the control node |
| Account | Admin role with XML API permission, or pre-generated API key |
| TLS | Self-signed cert OK; trust it on the control node or set `validate_certs: false` |

### Panorama device (when applicable)

Same as a firewall, plus:

- A `device_group` named scope for the objects/policies you'll manage.
- A `template` and/or `template_stack` named scope for network/device config.
- Managed firewalls registered and reachable for `panos_commit_push` to
  succeed.

---

## 3. Configuration Reference

This is the complete parameter reference. Parameters split into four groups:

1. **Provider parameters** — credentials and connection details (`provider`
   dict).
2. **Scope parameters** — where in PAN-OS the operation targets (`vsys`,
   `device_group`, `template`, …).
3. **State and movement parameters** — `state`, `location`, `audit_comment`,
   `target`, …
4. **Resource parameters** — fields specific to the object the module
   manages.

### 3.1 Provider parameters (`provider` dict)

Source: `plugins/doc_fragments/fragments.py`, `PROVIDER` fragment.

| Key | Type | Required | Default | Description |
|-----|------|----------|---------|-------------|
| `ip_address` | str | yes | — | IP/hostname of the firewall or Panorama |
| `username` | str | no | `admin` | Account name (ignored if `api_key` set) |
| `password` | str | no | — | Password (ignored if `api_key` set) |
| `api_key` | str | no | — | XML API key (use instead of user/pass) |
| `port` | int | no | `443` | TLS port |
| `serial_number` | str | no | — | If `ip_address` is a Panorama, target a managed firewall by serial |

The `transitional_provider` fragment also exposes the deprecated classic
params (`ip_address`, `username`, `password`, `api_key`, `port`) directly on
the task — these are kept for backwards compat but are scheduled for removal
in 4.0.0.

### 3.2 Scope parameters

| Parameter | Source fragment | Default | Description |
|-----------|-----------------|---------|-------------|
| `vsys` | `vsys` | `vsys1` | Firewall vsys for objects/interfaces |
| `vsys` | `vsys_shared` | `shared` | Vsys for shared services (syslog/snmp profiles) |
| `vsys` | `vsys_import` | (none) | Vsys to import into for importable objects |
| `vsys_dg` | `vsys_dg` | `vsys1` (NGFW) / `shared` (Pano) | Combined vsys+device-group |
| `device_group` | `device_group` | `shared` | Panorama device group |
| `template` | `template_only` / `full_template_support` | (none) | Panorama template |
| `template_stack` | `full_template_support` | (none) | Panorama template stack |
| `rulebase` | `rulebase` | `pre-rulebase` (Pano) / `rulebase` (NGFW) | Pre/Post rulebase |

### 3.3 State parameters

| Parameter | Source fragment | Choices | Default |
|-----------|-----------------|---------|---------|
| `state` | `state` | `present` / `absent` | `present` |
| `state` | `enabled_state` | `present` / `absent` / `enabled` / `disabled` | `present` |
| `state` | `network_resource_module_state` | `present` / `absent` / `replaced` / `merged` / `deleted` / `gathered` | `present` |

### 3.4 Movement and rule parameters (policy modules)

| Parameter | Source fragment | Choices | Description |
|-----------|-----------------|---------|-------------|
| `location` | `movement` | `top` / `bottom` / `before` / `after` | Position of the new rule |
| `existing_rule` | `movement` | str | When `location=before/after`, the relative rule |
| `target` | `target` | list | Restrict rule to specific firewall serials |
| `negate_target` | `target` | bool | Negate the `target` list |
| `audit_comment` | `audit_comment` | str | Audit message for the rule (only on change) |
| `uuid` | `uuid` | str | Read-only rule UUID (advanced) |
| `gathered_filter` | `gathered_filter` | str | Filter expression for `state: gathered` |

### 3.5 Per-module-family resource parameters

The collection has a few major module families. Below is the parameter set
for each.

#### Address-object family

| Module | Key parameters |
|--------|----------------|
| `panos_address_object` | `name`, `value`, `address_type` (`ip-netmask`/`ip-range`/`fqdn`/`ip-wildcard`), `description`, `tag` |
| `panos_address_group` | `name`, `static_value` (list), `dynamic_value` (str expression), `description`, `tag` |
| `panos_service_object` | `name`, `protocol` (`tcp`/`udp`), `source_port`, `destination_port`, `description`, `tag` |
| `panos_service_group` | `name`, `value` (list), `tag` |
| `panos_application_object` | `name`, `category`, `subcategory`, `technology`, `risk`, etc. |
| `panos_application_group` | `name`, `value` (list) |
| `panos_application_filter` | `name`, `category`, `subcategory`, `technology`, `risk`, etc. |
| `panos_tag_object` | `name`, `color` (40+ named choices) or `color_value` (XML), `comments` |
| `panos_schedule_object` | `name`, `disable_override`, `type` (recurring/non-recurring), schedule rules |
| `panos_custom_url_category` | `name`, `url_value`, `type`, `description` |
| `panos_dynamic_user_group` | `name`, `description`, `filter`, `tag` |
| `panos_edl` (External Dynamic List) | `name`, `source`, `repeat`, `description` |
| `panos_region` | `name`, `address`, `latitude`, `longitude` |

#### Policy family

| Module | Key parameters |
|--------|----------------|
| `panos_security_rule` | `rule_name`, `source_zone`, `source_ip`, `source_user`, `destination_zone`, `destination_ip`, `application`, `service`, `category`, `action` (`allow`/`deny`/`drop`/`reset-client`/`reset-server`/`reset-both`), `log_setting`, `log_start`, `log_end`, `description`, `rule_type` (`universal`/`intrazone`/`interzone`), `tag_name`, `negate_source`, `negate_destination`, `disabled`, `schedule`, `icmp_unreachable`, `disable_server_response_inspection`, `group_profile`, `antivirus`, `vulnerability`, `spyware`, `url_filtering`, `file_blocking`, `data_filtering`, `wildfire_analysis`, `group_tag` |
| `panos_nat_rule2` | `name`, `description`, `nat_type` (`ipv4`/`nat64`/`nptv6`), `from_zones`, `to_zones`, `to_interface`, `service`, `source_addresses`, `destination_addresses`, `source_translation_*` family (type, address_type, interface, ip_address, translated_addresses, fallback_*, static_*), `destination_translated_address`, `destination_translated_port`, `ha_binding` (`primary`/`both`/`0`/`1`), `disabled`, `tags`, `destination_dynamic_translated_*` family, `group_tag` |
| `panos_pbf_rule` | `name`, `description`, `tags`, `from_type` (`zone`/`interface`), `from_value`, `source_addresses`, `source_users`, `negate_source`, `destination_addresses`, `negate_destination`, `applications`, `services`, `schedule`, `disabled`, `action` (`forward`/`forward-to-vsys`/`discard`/`no-pbf`), `forward_*` family, `enable_enforce_symmetric_return`, `symmetric_return_addresses`, `group_tag` |
| `panos_decryption_rule` | `name`, `description`, source/destination zones+IPs, applications, action |
| `panos_match_rule` | Test traffic against rules (replaces `panos_query_rules`) |

#### Network family

| Module | Purpose |
|--------|---------|
| `panos_interface` | Ethernet data-port (`if_name`, `mode`, `ip`, `enable_dhcp`, `zone_name`, `vr_name`) |
| `panos_aggregate_interface` | Aggregate (LACP) interface |
| `panos_l2_subinterface`, `panos_l3_subinterface` | Sub-interfaces |
| `panos_loopback_interface`, `panos_tunnel`, `panos_vlan_interface` | Special interface types |
| `panos_zone` | Security zone (`zone`, `mode` `tap`/`virtual-wire`/`layer2`/`layer3`/`external`, `interface`, `zone_profile`, `enable_userid`) |
| `panos_vlan`, `panos_virtual_wire` | L2 plumbing |
| `panos_virtual_router` | Legacy VR (`name`, `interface`, `ad_*` distances) |
| `panos_logical_router`, `panos_logical_router_vrf` | New advanced-routing engine |
| `panos_logical_router_vrf_static_route`, `panos_logical_router_vrf_ospf_area` | Advanced routing details |
| `panos_static_route` | Static route on a VR (`name`, `destination`, `nexthop_type` `ip-address`/`discard`/`none`/`next-vr`, `nexthop`, `metric`, `virtual_router`, `interface`, `enable_path_monitor`, `failure_condition`) |
| `panos_redistribution` | Redistribution profile |
| `panos_bgp`, `panos_bgp_*` (12 BGP modules) | BGP configuration |
| `panos_ipsec_tunnel`, `panos_ipsec_profile`, `panos_ipsec_ipv4_proxyid` | IPsec |
| `panos_ike_gateway`, `panos_ike_crypto_profile` | IKE |
| `panos_gre_tunnel` | GRE |
| `panos_management_profile` | Interface management profile |
| `panos_dhcp`, `panos_dhcp_relay`, `panos_dhcp_relay_ipv6_address` | DHCP |

#### Device / system family

| Module | Purpose |
|--------|---------|
| `panos_facts` | Gather facts (`gather_subset` of `system`/`session`/`interfaces`/`ha`/`routing`/`vr`/`vsys`/`config`) |
| `panos_check` | Wait for autocommit / device readiness |
| `panos_op` | Run an arbitrary op command (`cmd`, `cmd_is_xml`, `ignore_disconnect`, `vsys`) |
| `panos_administrator` | Add/remove/rotate admin users |
| `panos_admin` (deprecated) | Subset of `panos_administrator` |
| `panos_admpwd` | Set admin password via SSH (used for first-boot) |
| `panos_api_key` | Generate an API key |
| `panos_cert_gen_ssh` | Generate certificates via SSH (first-boot) |
| `panos_lic` | License install |
| `panos_software` | Install PAN-OS version (`version`, `download`, `install`, `restart`, `sync_to_peer`, `timeout`, `named_config`, `perform_software_check`) |
| `panos_dynamic_updates` | Content / antivirus / app updates |
| `panos_restart` | Reboot the firewall |
| `panos_loadcfg` | Load a saved/named config |
| `panos_mgtconfig` | Management config (DNS, NTP, hostname) |
| `panos_active_in_ha` | Verify node is HA-active |
| `panos_ha` | HA configuration |
| `panos_readiness_checks` | Pre-upgrade readiness (panos-upgrade-assurance) |
| `panos_state_snapshot`, `panos_snapshot_report` | Pre/post upgrade snapshots |
| `panos_userid` | User-ID API |
| `panos_registered_ip`, `panos_registered_ip_facts` | Dynamic registered IPs |
| `panos_export` | Export config / certs / pcaps / tech-support (28 categories) |
| `panos_import` | Import config / certs / pcaps |
| `panos_config_element` | Raw XML config element get/set/edit/delete |
| `panos_type_cmd` | Raw type=keygen/op/config |
| `panos_vm_auth_key` | VM-Series auth-key |

#### Profile family

| Module | Purpose |
|--------|---------|
| `panos_log_forwarding_profile` (+ `_match_list`, `_match_list_action`) | Log forwarding |
| `panos_syslog_profile`, `panos_syslog_server` | Syslog server profile |
| `panos_snmp_profile`, `panos_snmp_v2c_server`, `panos_snmp_v3_server` | SNMP server profile |
| `panos_email_profile`, `panos_email_server` | Email server profile |
| `panos_http_profile`, `panos_http_profile_header`, `panos_http_profile_param`, `panos_http_server` | HTTP server profile |
| `panos_pg` (security profile group) | Profile group (virus/spyware/vulnerability/url/file/data/wildfire) |

#### Panorama family

| Module | Purpose |
|--------|---------|
| `panos_device_group` | Manage device groups (`name`, `parent`, devices) |
| `panos_template`, `panos_template_stack`, `panos_template_variable` | Templates |
| `panos_commit_panorama` | Commit Panorama itself |
| `panos_commit_push` | Push to managed devices (`style`, `name`, `admins`, `include_template`, `force_template_values`, `devices`, `sync`) |

### 3.6 Doc fragments inventory (16 total)

From `plugins/doc_fragments/fragments.py`:

| Fragment | Adds |
|----------|------|
| `provider` | The provider dict |
| `transitional_provider` | provider + classic params (deprecated) |
| `state` | `state: present/absent` |
| `enabled_state` | `state: present/absent/enabled/disabled` |
| `network_resource_module_state` | `state: present/absent/replaced/merged/deleted/gathered` |
| `vsys` | `vsys` (default `vsys1`) |
| `vsys_shared` | `vsys` (default `shared`) |
| `vsys_import` | `vsys` for imported objects |
| `vsys_dg` | combined `vsys_dg` |
| `device_group` | `device_group` (default `shared`) |
| `template_only` | `template` (Panorama) |
| `full_template_support` | `template` + `template_stack` |
| `rulebase` | `rulebase: pre-rulebase/rulebase/post-rulebase` |
| `deprecated_commit` | Legacy `commit: bool` flag (deprecated) |
| `uuid`, `movement`, `target`, `audit_comment`, `gathered_filter` | Policy-rule extras |

### 3.7 `meta/runtime.yml` deprecation routing

Twelve modules redirect to a successor and will be removed in `4.0.0`:

| Old | Redirected to | Reason |
|-----|---------------|--------|
| `panos_admin` | `panos_administrator` | Subset of admin module |
| `panos_commit` | `panos_commit_firewall` | Replaced by 3 specialised commits |
| `panos_dag` | `panos_address_group` | Idempotent rewrite |
| `panos_dag_tags` | `panos_registered_ip` | Modern API |
| `panos_object` | `panos_address_object` | Idempotent rewrite |
| `panos_object_facts` | (no redirect) | Replaced by `state: gathered` |
| `panos_nat_rule` | `panos_nat_rule2` | Network-resource rewrite |
| `panos_nat_rule_facts` | (no redirect) | Use `state: gathered` |
| `panos_query_rules` | `panos_match_rule` | Better implementation |
| `panos_sag` | `panos_address_group` | Static address group rewrite |
| `panos_security_rule_facts` | `panos_security_rule` | Use `state: gathered` |
| `panos_virtual_router_facts` | `panos_virtual_router` | Use `state: gathered` |
| `panos_zone_facts` | `panos_zone` | Use `state: gathered` |

---

## 4. Deployment & Scaling

### Single control node (default)

The simplest topology: one Linux host (laptop, jump server, CI runner) with
the collection installed, talking to one or many firewalls. No persistence,
no shared state — every run starts cold. Forks scale the parallelism:

```bash
ansible-playbook -i hosts.yml site.yml --forks 25
```

`forks` controls how many firewalls are configured in parallel. Each fork is
an isolated Python process, so there's no shared SDK state between them.

> [!TIP]
> For 100+ firewalls, set `forks: 50–100` and use `serial: <N>` in the playbook to batch the rollout. PAN-OS handles concurrent API requests well, but you can saturate Panorama if every firewall is doing a `commit_push` simultaneously.

### Ansible Automation Platform (AAP) / AWX

AAP is the supported enterprise deployment mode. Build an Execution
Environment (EE) container image:

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

Build with `ansible-builder build` and push to your registry. AAP job
templates point at the EE and execute playbooks on schedule or on demand.

### CI/CD scheduler

The collection is fully scriptable from CI. A typical GitLab/GitHub Actions
job:

```yaml
- name: Apply firewall config
  run: |
    pip install ansible-core>=2.16 pan-os-python panos-upgrade-assurance xmltodict dpath
    ansible-galaxy collection install paloaltonetworks.panos
    ansible-playbook -i inventory/prod site.yml \
      --vault-password-file <(echo "$ANSIBLE_VAULT_PW")
  env:
    PANOS_PASSWORD: ${{ secrets.PANOS_PASSWORD }}
    ANSIBLE_VAULT_PW: ${{ secrets.ANSIBLE_VAULT_PW }}
```

### Event-Driven Ansible (EDA)

The collection ships an EDA event source at
`extensions/eda/plugins/event_source/logs.py` (declared in
`meta/extensions.yml`). This lets a rulebook react to PAN-OS log events —
for example, auto-block a malicious IP after a threat log:

```yaml
# rulebook.yml
- name: Auto-block on threat
  hosts: all
  sources:
    - paloaltonetworks.panos.logs:
        host: 0.0.0.0
        port: 5514
  rules:
    - name: Block bad actor
      condition: event.threatid == 'malware-foo'
      action:
        run_playbook:
          name: playbooks/auto_block.yml
```

### Scaling concerns

| Concern | Mitigation |
|---------|------------|
| Slow Panorama commits at scale | Use `panos_commit_panorama` with `device_groups: [...]` to limit scope; chunk pushes with `serial:` |
| API rate limits | Keep `forks` ≤ 50 for a single Panorama; spread across multiple Panoramas if you have HA pairs |
| Long ops timing out | Bump `timeout` on `panos_software`, `panos_export`, `panos_op` |
| Fork-per-firewall RAM | ~50 MB per Python fork; budget accordingly |

---

## 5. Operations

### 5.1 Day-to-day runbook

| Task | Module(s) | Cadence |
|------|-----------|---------|
| Verify firewall reachability | `panos_check` | Daily smoke test |
| Pull config backup | `panos_export` (`category: configuration`) | Daily |
| Snapshot pre-change state | `panos_state_snapshot` | Per-change |
| Push config from Panorama | `panos_commit_panorama` + `panos_commit_push` | Per-change |
| Validate post-change | `panos_facts`, `panos_op` (`show jobs all`) | Per-change |
| Rotate API keys | `panos_api_key` + update vault | Quarterly |
| Pre-stage a new PAN-OS image | `panos_software` (`install: false`) | Per release |

### 5.2 Health checks

```yaml
- name: Daily health check
  hosts: firewalls
  connection: local
  gather_facts: false

  tasks:
    - name: Confirm firewall is responsive
      paloaltonetworks.panos.panos_check:
        provider: "{{ provider }}"
        timeout: 60

    - name: Gather session and HA facts
      paloaltonetworks.panos.panos_facts:
        provider: "{{ provider }}"
        gather_subset: ['system', 'session', 'ha']
      register: facts

    - name: Alert if HA out of sync
      ansible.builtin.fail:
        msg: "HA out of sync on {{ inventory_hostname }}"
      when:
        - ansible_net_ha_enabled | default(false)
        - ansible_net_ha_localstate not in ['active', 'passive']
```

### 5.3 Backup runbook

```yaml
- name: Daily firewall backups
  hosts: firewalls
  connection: local
  gather_facts: false

  tasks:
    - name: Export config
      paloaltonetworks.panos.panos_export:
        provider: "{{ provider }}"
        category: 'configuration'
        filename: "/var/backups/panos/{{ inventory_hostname }}-{{ ansible_date_time.date }}.xml"
        create_directory: true
```

> [!CAUTION]
> Backup XML files are sensitive — they contain admin password hashes, downstream credentials, and certificate private keys. See §7.4.

### 5.4 Monitoring integration

`panos_facts` returns a structured dict that maps cleanly to Prometheus
exporters or to a JSON-over-HTTP endpoint:

```yaml
- name: Publish metrics
  ansible.builtin.uri:
    url: "https://metrics.example.com/api/v1/panos"
    method: POST
    body_format: json
    body: "{{ ansible_facts | combine({'host': inventory_hostname}) | to_json }}"
```

> [!NOTE]
> The collection deliberately does not ship monitoring code — it produces facts; you decide how to ship them. Common patterns: write to Prometheus textfile collector, push to InfluxDB, or POST to an internal metrics API.

### 5.5 Disaster recovery

Recovery from "firewall config corrupted":

1. Find the most recent good backup XML.
2. SCP it to the firewall (or use the restore-via-import workflow).
3. `panos_op` with `cmd: '<load><config><from>good.xml</from></config></load>'`
   and `cmd_is_xml: true`.
4. `panos_commit_firewall`.

Recovery from "firewall offline / replaced":

1. Boot the new firewall to factory defaults.
2. `panos_admpwd` to set the admin password (via SSH).
3. `panos_lic` to install the license.
4. `panos_loadcfg` to load the previously exported running config.
5. `panos_commit_firewall` to activate.

> [!TIP]
> For a full unattended bootstrap, chain the modules above with `panos_check` between each step so the next module waits until the previous has finished.

### 5.6 Operational pre-flight checklist

- [x] `pan-os-python` installed on the control node
- [x] `panos-upgrade-assurance` installed (for the upgrade modules)
- [x] `provider` vars vaulted, never plaintext in playbook YAML
- [x] `ansible_python_interpreter` pinned in inventory
- [x] Collection version pinned in `requirements.yml`
- [ ] Backup runbook scheduled in CI
- [ ] EDA rulebook deployed for log-driven response (optional)

---

## 6. Performance

### 6.1 Where time goes

A rough breakdown for a typical "add 100 address objects + 50 rules + commit"
playbook against a real PA-VM:

| Phase | Wall time | Notes |
|-------|-----------|-------|
| Connection setup (`PanDevice.create_from_device`) | ~1.5 sec | Once per task |
| Per-object refresh + diff | ~80 ms | Each object module reads then writes |
| XML set request | ~50 ms | Each individual object create |
| Commit | 30 sec – 5 min | Depends on candidate size and device model |
| Commit push (Panorama → device) | 1–10 min | Per device |

### 6.2 Tuning levers

| Knob | Effect |
|------|--------|
| `forks: <N>` (Ansible) | Parallelize across hosts. ~50 is a safe ceiling for one Panorama. |
| `serial: <N>` (Ansible) | Roll out to N hosts at a time — reduces blast radius |
| `gather_subset` | Limit `panos_facts` to what you need; `config` is heavy |
| `sync: false` (commit modules) | Return immediately with a `jobid`; poll later |
| `timeout` | Per-module; bump for long ops |
| `poll_interval` / `poll_timeout` (`panos_op`) | XML API job polling |

### 6.3 Common performance pitfalls

- **`gather_subset: ['all']` against Panorama** — Panorama only supports
  `system`, `ha`, `config` subsets. Asking for `interfaces` is wasted time.
- **Many small commits** — batch all your changes in one play, then commit
  once. Each commit takes 30+ seconds on a real firewall.
- **`panos_op` in a tight loop** — every op is a full HTTP round trip plus a
  job poll. Consider whether one structured XML show command can replace 20
  CLI commands.
- **No `sync: false` on long pushes** — if you're pushing to 50 device
  groups, run them async and poll for completion at the end.

> [!NOTE]
> The 5 MB XML body limit (in `plugins/httpapi/panos.py:578`) is a hard cap. If you ever hit `Data too large for XML API request`, the only fix is to split the request — there's no server-side knob.

---

## 7. Security

### 7.1 Threat model

The control node holds credentials that have **full administrative control**
over PAN-OS firewalls and Panorama. A compromised control node = compromised
firewall fleet. Every recommendation below treats that as the central
threat.

### 7.2 Credential handling

- Never check unencrypted credentials into source control. Always use
  `ansible-vault encrypt_string`:

  ```bash
  ansible-vault encrypt_string --stdin-name 'vault_panos_pw'
  # paste password, ctrl-D
  ```
- Set `ANSIBLE_VAULT_PASSWORD_FILE` (or `--vault-password-file`) so you
  never type the master vault password into shell history:

  ```bash
  export ANSIBLE_VAULT_PASSWORD_FILE=$HOME/.ansible/vault_pw
  chmod 600 $HOME/.ansible/vault_pw
  ```
- Use `no_log: true` on every task that handles a secret. The collection's
  modules already mark the relevant parameters (`api_key`, `password`,
  `admin_password`, `admin_phash`, etc.) with `no_log=True` in their
  argument spec, but Ansible may still print parameter values via the
  `debug` task — `no_log: true` on the task is the belt-and-braces step:

  ```yaml
  - name: Set admin password
    paloaltonetworks.panos.panos_administrator:
      provider: "{{ provider }}"
      admin_username: 'automation'
      admin_password: "{{ vault_automation_pw }}"
      superuser: true
      state: present
    no_log: true
  ```

> [!IMPORTANT]
> `no_log: true` on a task suppresses parameter logging, but it does **not** suppress what your module returns in `register:`. If you `register: result` after a sensitive task, then `debug: var=result`, you may still print secrets. Audit your debugging.

### 7.3 Secrets handling

- **API keys** are bearer tokens equivalent to admin credentials. Treat them
  like passwords — vault them, rotate them, never commit them.
- **Admin password hashes** (`admin_phash`) are reusable: anyone who has the
  hash can paste it back in via `panos_administrator` and inherit that
  admin's role. Treat hashes like passwords.
- **Downstream service credentials** (LDAP, RADIUS, SNMPv3, syslog over TLS,
  email server passwords) are part of the device configuration, so a
  configuration export is sensitive too (see 7.4).

### 7.4 Protect configuration backups

Configuration backups taken with `panos_export` (`category: configuration`), with `panos_op`
(`<show><config><running></running></config></show>`) or with `HttpApi.get(xpath="/config")` are full XML copies of
the device configuration. They can include administrator password hashes, certificates and keys, and downstream
service credentials, so handle them as credential material:

- **Restrict and encrypt.** Limit ACLs to the backup operator (`chmod 600`), encrypt at rest (disk-level or
  per-file GPG), and keep them in a private bucket or share with server-side encryption and a retention policy.
- **Keep API keys on a cadence.** Rotate the automation API key on a known schedule (see 7.6) and after a backup
  has been handled outside the operator host.
- **Plan for exposure.** If a backup is ever exposed, follow your incident process: rotate the credentials and
  re-issue the certificates it contained.

For how PAN-OS protects stored secrets (including the master key), use the official PAN-OS administrator documentation as the reference, not this guide.

### 7.5 Network security

- The firewall management plane should never be exposed on the public
  Internet. Restrict TCP/443 to the control-node CIDR.
- Use TLS verification end-to-end. The `httpapi` plugin's
  `ansible_httpapi_validate_certs: false` is acceptable in lab; in
  production, install a real CA-signed cert on the firewall and enable
  validation.
- Restrict the firewall's management profile (or whole management vsys) to
  the control plane.
- For Panorama, separate the management network from the production data
  plane.

### 7.6 Authentication and authorization

- Use a dedicated `ansible` admin role with only the permissions Ansible
  needs. Don't use `superuser` if read-only would do.
- Rotate the API key quarterly via `panos_api_key` (or PAN-OS native
  `debug api-key`).
- Use authentication profiles (LDAP/SAML/RADIUS) for human admins; reserve
  local admins for automation accounts only.
- Enable PAN-OS audit logging for all API actions; ship audit logs to a
  SIEM.

### 7.7 Code-supply-chain security

- Pin the collection version in `requirements.yml` (`>=3.0.0,<4.0.0`).
- Pin Python deps in your control-node lock file. Don't blindly upgrade
  `pan-os-python` mid-release.
- `pan-os-python` is signed and published from PyPI by Palo Alto Networks.
  Verify with `pip download --require-hashes` if you mirror.
- Signed releases of the collection itself live on Galaxy and as GitHub
  release assets (signed via `semantic-release` per `.releaserc.json`).

### 7.8 Hardening checklist

- [x] All credentials in `ansible-vault encrypt_string`-protected vars
- [x] `no_log: true` on every task that handles a secret
- [x] Backup files have restricted ACLs (`chmod 600`, owned by backup
      operator only)
- [x] Backup files are encrypted at rest (disk-level or per-file GPG)
- [x] API keys are rotated on a known cadence (e.g., quarterly, plus
      after any backup leaves the operator host)
- [x] Vault password file has `chmod 600`
- [x] Per-firewall least-privilege admin role for automation (not
      `superuser`)
- [x] Firewall management plane locked to the control-node CIDR

---

## 8. Developer Internals

### 8.1 Repository layout

```text
pan-os-ansible/
├── galaxy.yml                  # Collection metadata: namespace, version, deps
├── meta/
│   ├── runtime.yml             # requires_ansible >=2.16; deprecation routing
│   └── extensions.yml          # EDA extension registration
├── plugins/
│   ├── modules/                # 118 task modules — the public surface
│   ├── module_utils/
│   │   └── panos.py            # ConnectionHelper + get_connection() (~1900 lines)
│   ├── httpapi/
│   │   └── panos.py            # HTTP API connection plugin (~640 lines)
│   └── doc_fragments/
│       └── fragments.py        # 16 reusable doc fragments
├── extensions/
│   └── eda/
│       ├── plugins/event_source/logs.py
│       └── rulebooks/decryption.yaml
├── tests/
│   ├── integration/            # Live-firewall idempotency tests
│   │   ├── README.md
│   │   ├── ansible.cfg
│   │   ├── inventory.example
│   │   ├── reset.yml           # Reset firewall to blank config
│   │   ├── run_all_tests.yml
│   │   ├── run_single_test.yml
│   │   ├── setup.yml
│   │   ├── firewall/test_panos_*.yml
│   │   ├── panorama/test_panos_*.yml
│   │   └── group_vars/all.yml
│   ├── unit/
│   │   ├── httpapi/test_panos.py
│   │   ├── plugins/module_utils/test_panos.py
│   │   └── plugins/modules/common/utils.py     # AnsibleExitJson harness
│   └── sanity/
│       └── ignore-2.16.txt … ignore-2.20.txt
├── docs/
│   ├── source/                 # Sphinx config for autodoc
│   ├── templates/module.rst.j2
│   └── contributing.md
├── requirements.txt
├── pyproject.toml
├── Makefile
├── README.md
├── CHANGELOG.md
└── .releaserc.json             # semantic-release config
```

### 8.2 Module structure: anatomy of a `panos_*` module

Every module follows the same five-section layout. Take
`plugins/modules/panos_address_object.py` as the canonical example:

1. **License header** — Apache 2.0.
2. **`DOCUMENTATION` string** — module name, short description, author,
   version_added, requirements, notes, doc fragments referenced via
   `extends_documentation_fragment`, and module-specific `options:`.
3. **`EXAMPLES` string** — copy-pasteable YAML examples.
4. **`RETURN` string** — what the module returns to the caller.
5. **`main()` function** — calls `get_connection()` to build a helper, then
   `helper.process(module)`.

Example (`panos_address_object.py`, abridged):

```python
def main():
    helper = get_connection(
        vsys=True,
        device_group=True,
        with_classic_provider_spec=True,
        with_network_resource_module_state=True,
        with_commit=True,
        with_gathered_filter=True,
        sdk_cls=("objects", "AddressObject"),
        sdk_params=dict(
            name=dict(required=True),
            value=dict(),
            address_type=dict(
                default="ip-netmask",
                choices=["ip-netmask", "ip-range", "fqdn", "ip-wildcard"],
                sdk_param="type",
            ),
            description=dict(),
            tag=dict(type="list", elements="str"),
        ),
    )

    module = AnsibleModule(
        argument_spec=helper.argument_spec,
        required_one_of=helper.required_one_of,
        supports_check_mode=True,
    )

    helper.process(module)
```

That's the entire module body. The `get_connection()` call is **declarative**:
it tells the helper which scope and capabilities to set up, and which SDK
class (`objects.AddressObject`) maps to which Ansible parameters.

### 8.3 `ConnectionHelper` deep dive

Source: `plugins/module_utils/panos.py`. The `ConnectionHelper` class is the
heart of the collection. Its lifecycle:

```python
class ConnectionHelper(object):
    def __init__(
        self,
        min_pandevice_version,
        min_panos_version,
        min_panos_upgrade_assurance_version,
        error_on_firewall_shared,
        panorama_error,
        firewall_error,
        template_is_optional,
    ):
        # Sets up argument_spec, required_one_of, and the
        # version-min checks for pan-os-python and PAN-OS itself.
        ...

    def get_pandevice_parent(self, module, timeout=0):
        # 1. Verify pan-os-python (and panos-upgrade-assurance) are
        #    installed at the required minimum version.
        # 2. Build the auth tuple from provider or classic params.
        # 3. PanDevice.create_from_device(*auth) — returns Firewall or
        #    Panorama instance.
        # 4. Verify min_panos_version against device's _version_info.
        # 5. Walk down to the right scope: vsys, device_group, template,
        #    rulebase. Returns the parent SDK object.
        ...

    def process(self, module):
        # The orchestrator. Calls:
        #   - get_pandevice_parent()
        #   - initial_handling(module)            (subclass hook)
        #   - parent_handling(parent, module)     (subclass hook)
        #   - sdk_cls.refresh(parent)
        #   - spec_handling(spec, module)         (subclass hook)
        #   - object_handling(obj, module)        (subclass hook)
        #   - pre_state_handling(obj, result, module)   (subclass hook)
        #   - apply_state(obj, listing, module, ..., result)
        #   - post_state_handling(obj, result, module)  (subclass hook)
        # Then exits the module with the result dict.
        ...

    def apply_state(self, obj, listing, module, ..., result):
        # The diff-and-apply engine. For each state:
        #   - present: create or update
        #   - absent / deleted: delete
        #   - replaced: full overwrite
        #   - merged: additive merge
        #   - gathered: read only, return matching items via
        #     matches_gathered_filter()
        ...

    def commit(self, module, include_template=False, admins=None):
        # The shared commit helper used by older modules' with_commit=True.
        # New modules use the dedicated commit modules instead.
        ...
```

### 8.4 Subclassing `ConnectionHelper` for module-specific behaviour

Many modules need extra logic — pre-flight, special spec handling, post-state
side-effects. They subclass `ConnectionHelper` and override hooks. Example:
`plugins/modules/panos_security_rule.py`:

```python
class Helper(ConnectionHelper):
    def initial_handling(self, module):
        # Migrate the deprecated "devicegroup" param to "device_group"
        if module.params["devicegroup"] is not None:
            module.deprecate(...)
            module.params["device_group"] = module.params["devicegroup"]

    def spec_handling(self, spec, module):
        # PAN-OS 10+ removed hip_profiles; supply a default for older
        # versions so we don't break existing playbooks.
        if module.params["state"] not in ("present", "replaced"):
            return
        if self.device._version_info < (10, 0, 0) and spec["hip_profiles"] is None:
            spec["hip_profiles"] = ["any"]

def main():
    helper = get_connection(
        helper_cls=Helper,         # custom subclass
        ...
    )
```

The hooks (in execution order):

1. `initial_handling(module)` — before any device interaction. Validate or
   migrate parameters.
2. `parent_handling(parent, module)` — after the parent SDK object is built,
   before the resource is fetched.
3. `spec_handling(spec, module)` — mutate the parameter dict before it is
   serialized to XML.
4. `object_handling(obj, module)` — mutate the SDK object before apply.
5. `pre_state_handling(obj, result, module)` — final hook before
   `apply_state`. Used for hashing passwords (e.g. `panos_administrator`).
6. `apply_state(...)` — the framework's diff-and-apply engine. Not usually
   overridden.
7. `post_state_handling(obj, result, module)` — after apply. Used for
   post-write side effects (e.g., refreshing API key after admin's own
   password change in `panos_administrator`).

### 8.5 Idempotency / diff logic

The framework computes idempotency by:

1. Fetching the live SDK object via `parent.refresh(obj)`.
2. Comparing each field in `sdk_params` between the desired spec and the
   live object.
3. If they match → `changed=False`, return.
4. If they differ → mutate the live object, call `obj.apply()` (or
   `obj.create()` / `obj.delete()` as appropriate), set `changed=True`.

The `state: replaced` mode is a full overwrite — fields not in the spec are
set to defaults / nulls. `state: merged` is additive.

`state: gathered` uses `matches_gathered_filter` from
`plugins/module_utils/panos.py:1220` to evaluate the user's filter
expression (e.g., `name == 'Web-Server-A'`) against each candidate object,
returning matching specs.

### 8.6 The HTTP API plugin (`plugins/httpapi/panos.py`)

The httpapi plugin is a separate code path that lets users configure the
collection via Ansible's `httpapi` connection model rather than the
provider dict. It's a 640-line module that:

- Caches the API key per connection (via `keygen` if not provided)
- Implements XML API verbs as Python methods: `show`, `get`, `set`, `edit`,
  `delete`, `rename`, `move`, `override`, `commit`, `commit_all`, `op`,
  `version`, `poll_for_job`
- Exposes `is_panorama()` to differentiate Firewall from Panorama
- Maps PAN-OS API error codes (`_PANOS_API_ERROR_CODES` dict) to a
  `PanOSAPIError` exception with code + name
- Enforces a 5 MB body limit (line 578) to match the XML API server-side
  cap
- Handles per-request auth via `update_auth()` — uses the `X-PAN-KEY`
  header on PAN-OS 9.0+

### 8.7 Test harness

#### Unit tests

Under `tests/unit/`. Use `pytest` plus a custom harness in
`tests/unit/plugins/modules/common/utils.py`:

```python
class ModuleTestCase:
    @pytest.fixture(autouse=True)
    def module_mock(self, mocker):
        return mocker.patch.multiple(
            basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json
        )

    @pytest.fixture
    def connection_mock(self, mocker):
        connection_class_mock = mocker.patch(
            "ansible_collections.paloaltonetworks.panos.plugins.module_utils.panos.Connection"
        )
        connection_class_mock.return_value.api_key.return_value = "foo"
        connection_class_mock.return_value.get_device_info.return_value = {
            "hostname": "PA-VM",
            "model": "PA-VM",
            "serial": "serial",
            "version": "10.0.0",
            "uptime": "uptime",
            "multivsys": "multivsys",
        }
        return connection_class_mock.return_value
```

The harness mocks out `AnsibleModule.exit_json` / `fail_json` so the test
can assert on the args without actually exiting the test process.
`connection_mock` provides a mock httpapi connection.

#### Integration tests

Under `tests/integration/`. These are **live-firewall** tests — they
require a real (or VM) PAN-OS device. The `tests/integration/README.md`
describes the expected initial config:

> - `ethernet1/1` in `outside` zone
> - `ethernet1/2` in `inside` zone
> - DHCP management configuration
> - a CA certificate with key named `local-ca` marked as `forward trust`

And for Panorama:

> - `Test-DG` device group
> - `Test-Template` template

Run a single test:

```bash
ansible-playbook -i inventory.ini run_single_test.yml -e test=test_panos_address_object
```

Run all tests:

```bash
ansible-playbook -i inventory.ini run_all_tests.yml
```

Each test pattern:

1. `import_tasks: 'reset.yml'` — restore from `blank.xml`.
2. Create the resource — assert `changed=True`.
3. Re-create with same params — assert `changed=False` (**idempotency**).
4. Modify the resource — assert `changed=True`.
5. Delete the resource — assert `changed=True`.
6. `panos_commit_firewall` — assert success.

> [!IMPORTANT]
> Idempotency is the contract every module must honour. A new module is not merge-ready until it passes the create-create-modify-delete-commit cycle in `tests/integration/`.

### 8.8 Adding a new module

The contributing workflow (per `docs/contributing.md`) is roughly:

1. Identify the SDK class in `pan-os-python` that maps to your PAN-OS
   resource (e.g., `panos.objects.AddressObject`,
   `panos.network.VirtualRouter`).
2. Copy a similar module (an existing simple one — `panos_tag_object.py` is
   a good starter) into `plugins/modules/panos_<resource>.py`.
3. Write the `DOCUMENTATION`, `EXAMPLES`, `RETURN` strings.
4. In `main()`, call `get_connection()` with the right
   `sdk_cls`, `sdk_params`, `vsys` / `device_group` / `template` flags,
   and any `with_*` capability flags (`with_commit`, `with_movement`,
   `with_uuid`, `with_target`, etc.).
5. If you need custom behaviour, subclass `ConnectionHelper` and override
   the appropriate hook (see §8.4).
6. Add an integration test under `tests/integration/firewall/` (and
   `panorama/` if applicable). Mirror the create-modify-delete-idempotency
   pattern.
7. Run `ansible-test sanity` from the collection root and fix any new
   warnings, or add to the relevant `tests/sanity/ignore-*.txt` if there's
   a justified pinning.
8. Open a PR using the `conventionalcommits` preset
   (`feat:` / `fix:` / `chore:` …) — `semantic-release` will use this to
   compute the next version number.

> [!TIP]
> Before writing a custom subclass, check the 16 doc fragments and the existing `with_*` flags. 90 % of new modules don't need a subclass — they only need the right declarative flags in `get_connection()`.

### 8.9 Sanity tests and Ansible-version compatibility

The collection is sanity-tested against Ansible Core 2.16 → 2.20. Each
target version has its own ignore-list at `tests/sanity/ignore-2.<N>.txt`
that lists known-acceptable warnings (mostly old-Python-syntax warnings
that don't affect runtime). When you add a module, run:

```bash
ansible-test sanity --python 3.10 --docker
```

Add new ignores only when you understand the warning and have agreed it's
acceptable.

### 8.10 Release pipeline

`.releaserc.json` configures `semantic-release` to:

1. Analyse commits since the last tag (`conventionalcommits` preset).
2. Compute the next semver version (patch / minor / major).
3. Update version in `galaxy.yml`, `pyproject.toml`,
   `docs/source/index.rst`.
4. Update `CHANGELOG.md`.
5. Tag and push.
6. Build the Galaxy tarball; publish to Ansible Galaxy and as a GitHub
   release asset.

The collection is shipped to:
`https://galaxy.ansible.com/ui/repo/published/paloaltonetworks/panos`.

### 8.11 Potential improvements

These are open opportunities visible in the source and issue tracker:

- **IPv6 in the advanced-routing modules.**
  `panos_logical_router_vrf_static_route` doesn't yet support IPv6 next-hop
  ([#660](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/660)).
- **Skip-version upgrade logic in `panos_software`.** The base-image
  detection ([#662](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/662))
  and post-install hang ([#658](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/658))
  could use better diagnostics.
- **Pretty-print XML in `panos_export`.** Issue
  [#636](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/636).
- **Multi-line `panos_op` commands.** Issue
  [#637](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/637).
- **Action groups in the collection.** Issue
  [#635](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/635) —
  group related modules for easier `collections:` use.
- **Inbound decryption for PAN-OS 10.2+.** Issue
  [#401](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/401).
- **Selective commit-by-admin in `panos_commit_panorama`.** Issue
  [#628](https://github.com/PaloAltoNetworks/pan-os-ansible/issues/628).

---

## 9. Integration Points

### 9.1 Galaxy / collection ecosystem

- **Galaxy URL:**
  `https://galaxy.ansible.com/ui/repo/published/paloaltonetworks/panos`
- **GitHub:** `https://github.com/PaloAltoNetworks/pan-os-ansible`
- **Documentation site (pan.dev):**
  `https://pan.dev/ansible/docs/panos`
- **Issue tracker:**
  `https://github.com/PaloAltoNetworks/pan-os-ansible/issues`
- **Galaxy tags:** `networking`, `paloaltonetworks`, `panw`, `security`,
  `panos` (per `galaxy.yml`).

### 9.2 SDK and adjacent libraries

- **`pan-os-python`** (`panos`) — the primary SDK the collection wraps.
  Same vendor; the collection pins `>=1.12.4,<2.0`.
- **`panos-upgrade-assurance`** — companion library for pre/post upgrade
  health checks. Used by `panos_active_in_ha`, `panos_readiness_checks`,
  `panos_snapshot_report`, `panos_state_snapshot`. Pinned `>=2.0.0,<3.0`.
- **`pan-python`** — older transitional SDK. Some legacy modules (e.g.,
  `panos_admin`, `panos_admpwd`) still import it.
- **`xmltodict`** — used by `panos_op` to convert XML responses to JSON.
- **`dpath`** — used by the `gathered_filter` evaluator.
- **`aiohttp`** — used by the EDA event source for async log streaming.

### 9.3 Ansible Automation Platform (AAP)

Build an Execution Environment with the collection + Python deps; create
job templates that point at it; use AAP's credentials store to inject
`PANOS_PASSWORD` / vault password.

### 9.4 Event-Driven Ansible (EDA)

The collection registers an EDA extension via `meta/extensions.yml`:

```yaml
extensions:
  - args:
      ext_dir: eda/plugins/event_filter
  - args:
      ext_dir: eda/plugins/event_source
```

The event source at `extensions/eda/plugins/event_source/logs.py` listens
for PAN-OS log events (typically forwarded as syslog) and emits structured
events to EDA rulebooks. Use case: auto-respond to a threat log by adding
the source IP to a registered-IP tag, then letting a Dynamic Address Group
block it.

### 9.5 SIEM / observability

- Forward the firewall's logs to Splunk, Sentinel, ELK, or any syslog
  collector via `panos_syslog_profile` + `panos_syslog_server`.
- Forward to an HTTP endpoint via `panos_http_profile` + `panos_http_server`
  (for, e.g., Datadog or generic webhook receivers).
- Forward to email via `panos_email_profile` + `panos_email_server`.
- Forward to SNMPv2c/v3 trap receivers via `panos_snmp_profile` +
  `panos_snmp_v2c_server` / `panos_snmp_v3_server`.

The Ansible side configures the firewall's log-forwarding plumbing; the
SIEM side is whatever you have.

### 9.6 Decryption broker / TLS inspection ecosystem

The `panos_decryption_rule` module manages decryption policy. Pair with
the EDA rulebook at `extensions/eda/rulebooks/decryption.yaml` for
event-driven decryption tuning.

### 9.7 Panorama → managed devices

Panorama is itself an integration point. The collection writes to Panorama
via `device_group` / `template` / `template_stack` parameters, then pushes
to managed devices via `panos_commit_push`. Push styles supported (per
`panos_commit_push.style`):

- `device group`
- `template`
- `template stack`
- `log collector group`
- `wildfire appliance`
- `wildfire cluster`

### 9.8 First-boot / day-zero pipelines

For VM-Series and PA-7000 first-boot bootstrapping, chain:

1. `panos_admpwd` (sets initial admin password via SSH; `panos_cert_gen_ssh`
   generates a cert via SSH).
2. `panos_lic` (license install).
3. `panos_dynamic_updates` (initial content install).
4. `panos_loadcfg` (load a baseline config from a saved file).
5. `panos_commit_firewall`.
6. `panos_check` (wait for ready).

### 9.9 Test data / sandboxing

Use `tests/integration/setup.yml` as a model for spinning up a test
firewall. The pattern: wait for the PAN-OS device to be ready (`panos_check`),
save a copy of the current config as `blank.xml` on the device, then run
your tests with the ability to reset back to blank between test cases.

### 9.10 Compliance / hardening references

The collection pairs cleanly with Palo Alto Networks' IronSkillet
configuration framework:

- IronSkillet provides the security-baseline configuration objects.
- Translate IronSkillet's XML into Ansible tasks using
  `panos_address_object`, `panos_security_rule`, `panos_zone`, etc.

Or use `panos_config_element` to push raw XML chunks if you need 1:1
parity with IronSkillet snippets while you migrate.

---

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