# pan-os-python Administrator & Developer Guide

This guide is for engineers who **integrate, deploy, and contribute to**
`pan-os-python` itself, or who run automation built on it at scale --
operations engineers, platform/SRE teams, security architects, and SDK
contributors. Architecture, the versioned object model, the wrapping
layer over `pan.xapi.PanXapi`, the exception hierarchy, deployment
patterns, operational concerns, security analysis, and how to extend the
SDK.

For task-oriented usage (connecting, adding rules, bulk operations), see
the **User Guide**.

---

## 1. Architecture Overview

`pan-os-python` is a thin, opinionated wrapper around the PAN-OS XML API.
It does five things:

1. **Builds a typed Python object tree** that mirrors the firewall's XML
   configuration (`PanDevice` -> `Vsys`/`DeviceGroup` -> `AddressObject`,
   `SecurityRule`, `Interface`, etc.).
2. **Translates the tree to XML and XPaths** automatically. You set
   `rule.action = "deny"`; the SDK figures out the right XPath, the right
   XML envelope, and the right API call (`type=set` vs `type=edit` vs
   `type=delete`).
3. **Tracks PAN-OS schema evolution** through per-class
   `VersionedParamPath` profiles, so the same Python class supports PAN-OS
   6.1 through 11.x by emitting different XPaths/elements based on the
   device's version.
4. **Wraps `pan.xapi.PanXapi`** -- the lower-level HTTP/XML transport
   library -- via `XapiWrapper`, classifying transport exceptions into
   the SDK's own exception hierarchy.
5. **Exposes operational surfaces** -- `userid`, `predefined`, `software`,
   `content`, `op()`, `commit()`, hit-count refresh, audit-comment logs,
   HA helpers, and the Panorama device-group hierarchy.

### Request flow (top to bottom)

```text
+-----------------------------------------------------------------+
|  Your script                                                    |
|     fw.add(rb).add(SecurityRule(name="x", action="deny"))       |
|     rule.create()                                               |
+-----------------------------------------------------------------+
                          |
                          v
+-----------------------------------------------------------------+
|  panos.base.VersionedPanObject                                  |
|     .create() -> .element() -> XML for this node + children     |
|     resolves XPath via parent chain & device version            |
+-----------------------------------------------------------------+
                          |
                          v
+-----------------------------------------------------------------+
|  panos.base.PanDevice.xapi                                      |
|     -> XapiWrapper.set/edit/delete/get/keygen/op                |
|     -> classify_exception() maps PanXapiError -> Pan*Error      |
+-----------------------------------------------------------------+
                          |
                          v
+-----------------------------------------------------------------+
|  pan.xapi.PanXapi (third-party 'pan-python==0.17.0')            |
|     HTTP/HTTPS to firewall management plane TCP/443             |
|     handles auth, retries, response parsing                     |
+-----------------------------------------------------------------+
                          |
                          v
+-----------------------------------------------------------------+
|  PAN-OS XML API on the firewall / Panorama mgmt interface       |
+-----------------------------------------------------------------+
```

### Class-tree backbone (confirmed in `panos/base.py`)

```text
PanObject
  +-- PanDevice                   (in panos/base.py)
  |     +-- Firewall              (panos/firewall.py)
  |     +-- Panorama              (panos/panorama.py)
  +-- VersionedPanObject          (everything that maps to PAN-OS XML)
        +-- VsysOperations        (objects that get imported into a vsys)
        |     +-- Vlan, Interface, VirtualWire, VirtualRouter,
        |         LogicalRouter, Vrf, ...
        +-- ValueEntry            (single-value entries, e.g. HTTP headers)
        +-- AddressObject, AddressGroup, ServiceObject, ServiceGroup,
            Tag, ApplicationObject, ApplicationGroup, ScheduleObject,
            Region, Edl, LogForwardingProfile, ...
        +-- SecurityRule, NatRule, DecryptionRule, AuthenticationRule,
            ApplicationOverride, PolicyBasedForwarding
        +-- Rulebase, PreRulebase, PostRulebase
        +-- Zone, EthernetInterface, AggregateInterface,
            Layer2Subinterface, Layer3Subinterface, ManagementProfile,
            IkeGateway, IpsecTunnel, IpsecCryptoProfile,
            IkeCryptoProfile, GreTunnel, Dhcp, ...
        +-- DeviceGroup, Template, TemplateStack, TemplateVariable
        +-- HighAvailability, HA1, HA2, HA3
        +-- CloudServicesPlugin, RemoteNetwork, RemoteNetworks,
            AggBandwidth, Tenants (panos/plugins.py)
```

Helper / non-config classes:

```text
VarPath, ParamPath, VersionedParamPath, VersioningSupport,
VersionedStubs, ParentAwareXpath  (XPath/version dispatch in panos/base.py)
OpState, OpStateContainer         (operational-state plumbing)
Predefined                        (panos/predefined.py)
SoftwareUpdater, ContentUpdater   (panos/updater.py)
UserId                            (panos/userid.py)
XapiWrapper                       (panos/base.py)
```

### Component inventory

| Component | Path | Role |
|---|---|---|
| Core base classes | `panos/base.py` | `PanObject`, `VersionedPanObject`, `PanDevice`, `XapiWrapper`, XPath/version machinery |
| Firewall device | `panos/firewall.py` | `Firewall`, `FirewallState`, `FirewallCommit`, vsys handling |
| Panorama device | `panos/panorama.py` | `Panorama`, `DeviceGroup`, `Template`, `TemplateStack`, `TemplateVariable`, commit-all |
| Policies | `panos/policies.py` | `Rulebase`, `PreRulebase`, `PostRulebase`, `SecurityRule`, `NatRule`, `DecryptionRule`, `AuthenticationRule`, `ApplicationOverride`, `PolicyBasedForwarding`, hit count, audit comments |
| Objects | `panos/objects.py` | `AddressObject`, `AddressGroup`, `ServiceObject`, `ServiceGroup`, `Tag`, `ApplicationObject`/`Group`/`Filter`/`Container`/`Tag`, `SecurityProfileGroup`, `CustomUrlCategory`, `LogForwardingProfile`, `DynamicUserGroup`, `ScheduleObject`, `Region`, `Edl` |
| Network | `panos/network.py` | `Zone`, `Vlan`, `EthernetInterface`, `AggregateInterface`, subinterfaces, `VirtualRouter`, `LogicalRouter`, `Vrf`, OSPF/BGP/RIP, IPsec/IKE, GRE, DHCP, advanced routing |
| Device | `panos/device.py` | `Vsys`, `SystemSettings`, NTP, admins, syslog/SNMP/email/HTTP/LDAP server profiles, certificate profiles, SSL decrypt |
| HA | `panos/ha.py` | `HighAvailability`, `HA1`/`HA2`/`HA3`, peer management |
| Errors | `panos/errors.py` | 22 exception classes rooted at `PanDeviceError` |
| Updater | `panos/updater.py` | `SoftwareUpdater`, `ContentUpdater` |
| User-ID | `panos/userid.py` | `UserId` subsystem (login/logout/register/tag) |
| Predefined | `panos/predefined.py` | Predefined applications/services/tags |
| Plugins | `panos/plugins.py` | Prisma Access / Cloud Services plugin objects |

### Technology stack

| Layer | Technology | Notes |
|---|---|---|
| Language | Python 3.6+ | Per `panos/__init__.py` (`RuntimeWarning` on older versions) |
| Transport | `pan-python==0.17.0` | Hard pin via `requirements.txt` |
| XML | `xml.etree.ElementTree` (stdlib) | Used throughout `panos/base.py`, `panos/policies.py`, etc. |
| Versioning | `distutils.version.LooseVersion` | Subclassed to `PanOSVersion` (Python 3.12+ caveat per issues #580, #529) |
| Logging | stdlib `logging` | With four extra debug levels (`DEBUG1`-`DEBUG4`) |
| Build | `pyproject.toml` (Poetry) | Release automation via `.releaserc.json` (semantic-release) |
| Docs | Sphinx + `m2r` + `sphinx_rtd_theme` | Read the Docs build at PaloAltoNetworks/pan-os-python |

> [!NOTE]
> The SDK does NOT use `requests`, `httpx`, `aiohttp`, or any async I/O.
> All transport runs through `pan.xapi.PanXapi`, which uses Python's
> stdlib `urllib`. Concurrency is the caller's responsibility -- use
> threading or multiprocessing.

---

## 2. System Requirements

### Runtime

| Requirement | Value |
|---|---|
| Python | 3.6+ (older versions emit `RuntimeWarning`) |
| OS | Any platform CPython runs on |
| Memory | Trivial -- typical scripts use 50-200 MB; large `refreshall` against complex Panoramas can spike to 1-2 GB |
| Network | Outbound TCP/443 to firewall/Panorama management interface |
| Permissions | Admin account on device, or pre-issued API key |

### Build / development

| Requirement | Value |
|---|---|
| Poetry | Recommended (per `pyproject.toml` + `poetry.lock`) |
| `pip install -e .` | Supported alternative |
| `mypy.ini` | Static type checking config present |
| `.flake8`, `.isort.cfg` | Linting/formatting profiles |
| `tox.ini` | Multi-Python test matrix |
| `.bandit` | Security linter config |
| Sphinx | For doc builds (see `docs/conf.py`, `docs/Makefile`) |

### Hard dependency

```text
pan-python==0.17.0  (cryptographically pinned via SHA-256 hashes in requirements.txt)
```

This is the **only** runtime dependency. There are no optional extras.

> [!IMPORTANT]
> The pin on `pan-python==0.17.0` is exact. Mixing in a different
> version (e.g., a system package or another package's transitive
> requirement) results in subtle XML-handling drift. Always install
> the SDK in an isolated virtualenv.

---

## 3. Configuration Reference

The SDK has no config files. Every knob is a constructor argument or a
module-level Python attribute. This section enumerates them.

### `panos.base.PanDevice` (abstract base)

```text
PanDevice(
    hostname,                    # required: hostname or IP
    api_username=None,           # admin username
    api_password=None,           # admin password
    api_key=None,                # pre-issued API key
    port=443,                    # mgmt-plane TCP port
    is_virtual=None,             # True for VM-Series; auto-detected
    timeout=1200,                # async-job seconds
    interval=0.5,                # async-job poll interval
    *args, **kwargs,
)
```

| Param | Type | Default | Description |
|---|---|---|---|
| `hostname` | `str` | (required) | Hostname or IP of the device |
| `api_username` | `str` | `None` | Admin username; required if no `api_key` |
| `api_password` | `str` | `None` | Admin password; paired with `api_username` |
| `api_key` | `str` | `None` | Pre-issued API key; bypasses `keygen` |
| `port` | `int` | `443` | Management TCP port |
| `is_virtual` | `bool` | `None` | Hint for VM-Series detection |
| `timeout` | `int` | `1200` | Seconds for synchronous long jobs (commit, syncreboot) |
| `interval` | `float` | `0.5` | Polling interval for async jobs |

### `panos.firewall.Firewall`

```text
Firewall(
    hostname=None,
    api_username=None,
    api_password=None,
    api_key=None,
    serial=None,                 # serial of the firewall
    port=443,
    vsys=None,                   # 'vsys1', ..., 'shared', or None
    is_virtual=None,
    multi_vsys=None,
    *args, **kwargs,
)
```

Additional kwargs accepted:

| Kwarg | Description |
|---|---|
| `vsys_name` | Display name of the vsys |
| `serial_ha_peer` | Serial of the HA peer |
| `management_ip` | Management IP override |

Class-level attributes:

| Attribute | Default | Purpose |
|---|---|---|
| `XPATH` | `"/devices"` | Root XPath for the firewall |
| `ROOT` | `Root.MGTCONFIG` | Where this object lives in the config tree |
| `SUFFIX` | `ENTRY` | XML element naming convention |
| `NAME` | `"serial"` | Field used as primary identifier |
| `DEFAULT_VSYS` | `"vsys1"` | vsys used when none is specified |

`CHILDTYPES`: ~50 entries spanning `device.*`, `objects.*`,
`policies.*`, `network.*`, and `ha.HighAvailability`. See
`panos/firewall.py` for the canonical list.

### `panos.panorama.Panorama`

```text
Panorama(
    hostname,
    api_username=None,
    api_password=None,
    api_key=None,
    port=443,
    *args, **kwargs,
)
```

Class-level attributes:

| Attribute | Default | Purpose |
|---|---|---|
| `FIREWALL_CLASS` | `firewall.Firewall` | Class used when instantiating managed firewalls |
| `NAME` | `"hostname"` | Primary identifier |
| `DEFAULT_VSYS` | `"shared"` | vsys placeholder for Panorama-shared scope |

`CHILDTYPES`: includes `firewall.Firewall`, `panorama.DeviceGroup`,
`panorama.Template`, `panorama.TemplateStack`, plus shared `objects.*`
and a subset of `device.*` server-profile classes.

### `panos.panorama.DeviceGroup`

```text
DeviceGroup(
    name,                        # device-group name
    tag=None,                    # list of tags
)
```

| Param | Type | Description |
|---|---|---|
| `name` | `str` | Device-group name (required, primary key) |
| `tag` | `list[str]` | Administrative tags |

### `panos.panorama.Template`

```text
Template(
    name,
    description=None,
    devices=None,                # list of serial numbers
    default_vsys=None,           # 7.0+ ; legacy
    multi_vsys=None,             # 6.1 and lower
    mode=None,                   # 6.1 and lower
    vpn_disable_mode=None,       # 6.1 and lower
)
```

### `panos.panorama.TemplateStack`

```text
TemplateStack(
    name,
    description=None,
    templates=None,              # list of template names in stack order
    devices=None,                # list of serial numbers
)
```

### `panos.policies.SecurityRule`

The most-used rule class. Constructor accepts (per `panos/policies.py`):

```text
SecurityRule(
    name,
    fromzone=["any"], tozone=["any"],
    source=["any"], source_user=["any"],
    hip_profiles=["any"],            # PAN-OS 10.0.0- only
    destination=["any"], application=["any"],
    service="application-default",   # default value, can be a list
    category=["any"],
    action=None,                     # 'deny','allow','drop','reset-client','reset-server','reset-both'
    log_setting=None,
    log_start=None, log_end=None,
    description=None,
    type="universal",                # 'universal', 'intrazone', 'interzone'
    tag=None,
    negate_source=None, negate_destination=None,
    disabled=None,
    schedule=None,
    icmp_unreachable=None,
    disable_server_response_inspection=None,
    group=None,                      # security profile group
    negate_target=None, target=None, # Panorama device-group only
    virus=None, spyware=None, vulnerability=None,
    url_filtering=None, file_blocking=None,
    wildfire_analysis=None, data_filtering=None,
    uuid=None,                       # PAN-OS 9.0+
    source_devices=["any"],          # PAN-OS 10.0+
    destination_devices=["any"],     # PAN-OS 10.0+
    group_tag=None,                  # PAN-OS 9.0+
)
```

Operational state subobjects:

- `rule.opstate.audit_comment` -- `RuleAuditComment` (PAN-OS 9.0+)
- `rule.opstate.hit_count` -- `HitCount` (PAN-OS 8.1+)

`HIT_COUNT_STYLE = "security"` is set on the class, used by
`RulebaseHitCount.refresh(style="security", ...)`.

### `panos.policies.NatRule`

NatRule has 30+ source-translation/destination-translation/dynamic
parameters. Highlights (per `panos/policies.py`):

```text
NatRule(
    name, description=None,
    nat_type="ipv4",                # 'ipv4', 'nat64', 'nptv6'
    fromzone=["any"], tozone=None,
    to_interface=None,
    service="any",
    source=["any"], destination=["any"],
    source_translation_type=None,   # 'dynamic-ip-and-port', 'dynamic-ip', 'static-ip'
    source_translation_address_type=None,
    source_translation_interface=None,
    source_translation_ip_address=None,
    source_translation_translated_addresses=None,
    source_translation_fallback_type=None,
    ...                              # ~14 more source-translation params
    destination_translated_address=None,
    destination_translated_port=None,
    ha_binding=None,                # 'primary','both','0','1'
    disabled=None,
    negate_target=None, target=None,
    tag=None,
    destination_dynamic_translated_address=None,  # PAN-OS 8.1+
    destination_dynamic_translated_port=None,     # PAN-OS 8.1+
    destination_dynamic_translated_distribution=None,
    uuid=None,                                     # PAN-OS 9.0+
    group_tag=None,                                # PAN-OS 9.0+
)
```

### `panos.objects.AddressObject`

```text
AddressObject(
    name,
    value,                          # IP, range, wildcard, or fqdn
    type="ip-netmask",              # 'ip-netmask','ip-range','ip-wildcard','fqdn'
    description=None,
    tag=None,                       # list of tag names
)
```

> [!NOTE]
> `ip-wildcard` was added in PAN-OS 9.0. Older devices reject it -- the
> SDK will pass the value through; the firewall returns the validation
> error.

### `panos.objects.AddressGroup`

```text
AddressGroup(
    name,
    static_value=None,              # list of address names (static group)
    dynamic_value=None,              # tag-match expression (dynamic group)
    description=None,
    tag=None,
)
```

A given group is either static (uses `static_value`) or dynamic (uses
`dynamic_value`) -- not both.

### `panos.objects.Tag`

```text
Tag(
    name,
    color=None,                     # 'color1' through 'color42' (gaps); use Tag.color_code()
    comments=None,
)
```

`Tag.color_code(color_name)` resolves a human color (`"red"`,
`"forest green"`, `"midnight blue"`, etc.) into the `colorN` ID PAN-OS
expects.

### `panos.objects.ServiceObject`

```text
ServiceObject(
    name,
    protocol="tcp",                 # 'tcp', 'udp', or (8.1+) 'sctp'
    source_port=None,
    destination_port=None,
    description=None,
    tag=None,
    enable_override_timeout=None,    # PAN-OS 8.1+
    override_timeout=None,
    override_half_close_timeout=None,
    override_time_wait_timeout=None,
)
```

### `panos.objects.ApplicationObject`

Has 30+ attributes describing application signatures -- category,
subcategory, technology, risk, port-list, evasion behaviors, file
types. See `panos/objects.py` (line ~320).

### Logging

Module-level constants in `panos/__init__.py`:

```python
panos.DEBUG1   # logging.DEBUG - 1 (method tracing)
panos.DEBUG2   # DEBUG1 - 1       (XML payloads)
panos.DEBUG3   # DEBUG2 - 1       (deeper XML)
panos.DEBUG4   # DEBUG3 - 1       (everything)
```

`panos.getlogger(name)` returns a configured logger with
`debug1`/`debug2`/`debug3`/`debug4` convenience methods.

> [!IMPORTANT]
> Production code should run at `WARNING` or `INFO`. The deeper levels
> emit credentials and certificate material in cleartext. See **Section 7**.

---

## 4. Deployment & Scaling

### Deployment patterns

| Pattern | Where the script runs | Trade-offs |
|---|---|---|
| **Direct from a workstation** | Engineer's laptop or jump host | Simple; tied to a human session |
| **Containerized cron** | A long-running container with cron and the SDK | Idempotent, observable via container logs |
| **CI/CD job** | GitHub Actions, GitLab CI, etc. | Auditable history of every config change |
| **Workflow engine** | Airflow, Argo, Temporal, AWX/Tower | Retries, scheduling, distributed execution |
| **Serverless** | AWS Lambda, Cloud Run | Cold-start cost; size limits OK (small image) |
| **Bastion / mgmt-only host** | A locked-down jump host with mgmt access | Most secure; standard for production |

The SDK has no daemon, no agent, no outbound listening port. It runs on
demand, makes API calls, exits.

### Scaling concurrency

The SDK is **synchronous** -- one HTTP request at a time per
`PanDevice` instance. Two scaling axes:

1. **Many devices in parallel.** Instantiate one `Firewall`/`Panorama`
   per worker thread or process. Each instance has its own
   `XapiWrapper` and its own API key.
2. **Many objects per device.** Use `create_similar()`,
   `apply_similar()`, `delete_similar()` instead of looping `create()`
   /`apply()`/`delete()`. One call instead of N. The bundled
   `bulk_address_objects.py` example creates 600 objects in one round
   trip.

> [!TIP]
> For Panorama with 100+ managed firewalls, use a `concurrent.futures`
> thread pool with one `Firewall` instance per worker. The Panorama
> push API (`commit_all`) is itself parallel server-side, so the
> client-side win is mostly about parallelizing the *fetches*.

### Sizing the pool

| Device count | Recommended workers | Notes |
|---|---|---|
| 1-10 | 4 | Single-threaded is fine; 4 keeps total wall time reasonable |
| 10-50 | 8-16 | Watch firewall mgmt-plane CPU |
| 50-200 | 16-32 | Beyond this, you're stressing Panorama; consider using `Panorama.refresh_devices()` once and caching |
| 200+ | Custom | Talk to Palo Alto support before going wide; mgmt plane is not built for this |

### Connection-pool settings

`PanXapi` reuses a single HTTPS connection per instance. There's no
explicit pool tuning. Long-running scripts should re-instantiate the
device object every few hours to avoid stale TLS sessions.

### Job/timeout tuning

| Setting | Default | Tune up if... |
|---|---|---|
| `Firewall(timeout=1200)` | 20 min | Commits on large Panoramas can exceed the default in extreme cases |
| `Firewall(interval=0.5)` | 0.5 s | Lower for small firewalls (faster polling); raise for very large pushes |
| `commit(sync=True)` | blocks | Use `sync_all=True` on Panorama to also wait for downstream device jobs |

> [!NOTE]
> A "commit" on Panorama is *local* -- it commits the candidate to the
> Panorama running config. To push to managed firewalls, call
> `Panorama.commit_all(...)`. Set `sync_all=True` if you want to block
> until the per-device push jobs complete.

---

## 5. Operations

### Day-2 runbooks

#### Health check

A minimal "is it alive" probe:

```python
from panos.firewall import Firewall

fw = Firewall(host, api_key=key)
print(fw.refresh_system_info())     # version, model, serial
print(fw.show_system_resources())   # CPU/memory
print(fw.is_ready())                # True if mgmt plane is up
```

`is_ready(minutes=10, seconds=0)` blocks until the device responds (up
to the timeout) -- useful right after a reboot.

#### Backup a firewall config

There's no first-class `backup()` method, but the pattern is one
op-command:

```python
xml = fw.op("show config running")
with open(f"{fw.serial}-{date.today()}.xml", "wb") as f:
    f.write(ET.tostring(xml.find(".//config"), encoding="utf-8"))
```

> [!CAUTION]
> The dumped XML contains every secret on the box -- `phash` admin
> password hashes, certificate private keys, IPsec PSKs, RADIUS shared
> secrets. Treat backup files as Tier-1 secrets. Encrypt at rest. See
> **Section 7**.

#### Lock-take, modify, lock-release

```python
fw.add_config_lock(comment="rule push by automation", exceptions=True)
try:
    # ... mutate the tree ...
    fw.commit(sync=True, exception=True)
finally:
    fw.remove_config_lock()
```

Locks block other admins from concurrent commits. `add_commit_lock()`
is the sibling for commit-time locking.

#### HA awareness

```python
fw.refresh_ha_active()     # query active/passive state
fw.is_active()             # bool
fw.toggle_ha_active()      # force failover (read code first!)
fw.synchronize_config()    # force HA config sync
fw.config_sync_state()     # check sync status
fw.config_synced()         # bool
```

`Firewall.set_ha_peers(device)` wires together two `Firewall`
instances so you can call `map_ha(method_name)` -- the method runs
against the active peer automatically.

### Monitoring

| Signal | Source | What it tells you |
|---|---|---|
| `fw.refresh_system_info()` | `<show><system><info>` | Liveness, version |
| `fw.show_system_resources()` | `<show><system><resources>` | CPU/memory |
| `pano.refresh_devices()` | Panorama | Per-managed-device connectivity |
| `pano.commit_all(...)` exit | `commit-all` job result | Push success/failure |
| `RulebaseHitCount.refresh()` | Per-rulebase op | Stale-rule detection |
| `fw.config_sync_state()` | `<show><high-availability><state>` | HA drift |

> [!TIP]
> Pipe `refresh_system_info()` into a TSDB once a minute. It's cheap
> and gives you a second heartbeat independent of SNMP/syslog.

### Periodic maintenance

| Cadence | Task | Why |
|---|---|---|
| Hourly | Health check across fleet | Early warning |
| Daily | Backup running config | Recovery |
| Weekly | Hit-count refresh, identify zero-hit rules | Policy hygiene |
| Monthly | API key rotation | Credential hygiene |
| Quarterly | Audit admin accounts via `device.Administrator.refreshall()` | Access review |

### Common operational pitfalls

- **Forgetting to commit.** `create()` writes to candidate, not running.
  Schedule a sync `fw.commit(sync=True)` after batches.
- **Calling `apply()` on a stale tree.** If you only refreshed one
  child but call `apply()` on the parent, you can wipe siblings. Refresh
  the full sibling set first.
- **Blocking on async pushes.** `commit_all` without `sync_all=True`
  returns once Panorama accepts the job; the per-device jobs run
  asynchronously. Always poll or `sync_all=True` if your script must
  know they're done.
- **API rate / connection limits.** Mgmt-plane CPU is the bottleneck;
  exceeding ~30 concurrent API calls per firewall causes the device to
  queue or reject.

---

## 6. Performance

### Where the time goes

For a typical "fetch all rules, find the matching one, modify, push"
script:

| Phase | Wall-clock |
|---|---|
| `keygen` (first call) | 100-300 ms |
| `refreshall(SecurityRule)` (typical rulebase) | 200 ms - 5 s |
| Per-rule `.apply()` / `.create()` | 200-500 ms each |
| `commit(sync=True)` | 5 s - 10+ min |
| `commit_all(sync_all=True)` (Panorama) | 30 s - 60+ min |

`commit_all` dominates Panorama workflows. Optimize by batching --
make all changes, then commit once.

### Bulk operations are non-negotiable for scale

`create()` over 600 address objects: ~2 minutes (one HTTP round trip
per object).
`create_similar()` over the same 600: ~3 seconds (one HTTP round
trip total).

The bundled `examples/bulk_address_objects.py` demonstrates and times
this. Use bulk for every scenario where you're touching > ~10 objects
of the same class.

### `refreshall` strategies

```python
# Default: pull every entry with all params -> verbose XML
SecurityRule.refreshall(rb)

# Just names, no params -> tiny XML, fast
SecurityRule.refreshall(rb, name_only=True)

# Don't add to the tree -> useful for read-only audits
SecurityRule.refreshall(rb, add=False)

# Hit running config instead of candidate
SecurityRule.refreshall(rb, running_config=True)
```

`name_only=True` is a 5-50x speedup for "discovery" workloads -- list
what's there, then individually `refresh()` only what you'll modify.

### Avoid re-instantiation churn

- One `Firewall(...)` instance per device per script. Don't
  re-instantiate on every iteration of a loop -- `keygen` is paid every
  time you do.
- `predefined`, `software`, `content`, `userid` are created lazily as
  attributes of `PanDevice.__init__`. They're cheap, but reusing the
  parent device object is still cheaper.

### Panorama scaling

`Panorama.refresh_devices(devices=None, only_connected=True,
expand_vsys=True, include_device_groups=True, add=True,
running_config=False)` is one big call that gets the whole managed
device list, their vsyses, and their device-group attachments. Use it
once at the top of a Panorama-wide script and walk the resulting tree
in memory.

> [!NOTE]
> `expand_vsys=True` materializes one `Firewall` instance per vsys per
> managed device. On a Panorama managing 100 multi-vsys firewalls, you
> get hundreds of objects. Pass `expand_vsys=False` if you only need
> per-chassis data.

---

## 7. Security

### Credential handling

The SDK keeps API credentials in **process memory only**. No file is
written, no environment variable is exported on its own. After
`keygen`, `fw.api_key` is set on the `PanDevice` instance and stays
there for the life of the process.

Three rules:

1. **Pull credentials from a secret manager** (Vault, AWS Secrets
   Manager, Azure Key Vault, GCP Secret Manager, 1Password CLI). Inject
   as environment variables for the duration of the script.
2. **Use API keys, not passwords**, in production scripts.
   Username/password is fine for a one-off interactive run.
3. **Rotate API keys regularly.** Every API key is a long-lived
   credential -- if compromised, an attacker has full mgmt-plane access
   under that admin's role until the key is revoked.

> [!WARNING]
> Never commit `.env` files containing PAN-OS credentials. Add
> `.env`, `*.api_key`, and `secrets.*` to your repo's `.gitignore`.
> Audit your CI logs, and keep API keys out of exception messages
> and stack traces.

### TLS verification

By default `pan.xapi.PanXapi` performs full TLS verification against
the system trust store. The SDK does not expose a "skip verification"
toggle.

- **Production.** Install your enterprise CA into the OS trust store,
  or set `REQUESTS_CA_BUNDLE`/`SSL_CERT_FILE` to point at a bundle
  containing the firewall's CA.
- **Lab.** If a self-signed cert is genuinely unavoidable for a one-off
  test, the documented workaround is to monkey-patch the SSL context
  on `pan.xapi.PanXapi`. Do this in the script itself, never as a
  global setting.

> [!CAUTION]
> Disabling TLS verification removes any defense against MITM attack
> on the management plane. An attacker with access to the management
> network could harvest credentials on the first connection. Don't
> normalize "skip verification" as your team's default.

### Protecting exported configurations

A configuration exported from a firewall or Panorama (for example with `op("show config running")`, or by walking
the SDK tree and serializing it) is sensitive material: it can include administrator password hashes, certificates
and keys, and the credentials of the services the device connects to (LDAP, RADIUS, syslog and similar).

- Store exports like other credential material: encrypted at rest, access limited to the people who need them,
  deleted when change management no longer needs them.
- If an export is shared outside your team (for example with a vendor for support), remove the sensitive values
  first and follow your data-handling policy.
- Rotate the API keys your scripts use on a known cadence (see *Credential handling* above).

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

### What NOT to log

- **API keys** -- redact `fw.api_key` before logging objects.
- **Passwords** -- if you use username/password mode, never log the
  `Firewall` instance or its `__dict__`; `_api_password` is stored as a
  private attribute.
- **`DEBUG3` / `DEBUG4` traces** -- these contain full request bodies,
  including candidate-config snippets with credentials. Capture only
  to a local file you control; never to a SIEM/log aggregator.
- **Stack traces from `PanCommitFailed`** -- the `.result` attribute
  can contain the snippet that failed validation, which may include a
  pre-shared key you were trying to set.

> [!WARNING]
> Configure your global Python logging filter or `logging.Filter`
> subclass to scrub `api_key=`, `api_password=`, and `phash=` patterns
> from any log record before it leaves the host.

### Authorization model

The SDK has no opinion about admin roles -- it uses whatever role is
attached to the API key/admin you authenticated as. To limit blast
radius, create a dedicated admin role on the firewall (XML API
permission scope, no GUI) and use that for automation. Avoid using
superuser admin accounts in scripts.

`device.Administrator` is the SDK class that manages admin accounts
themselves; review with `device.Administrator.refreshall(fw)`.

### Network exposure

`pan-os-python` does not listen on any port. There's no inbound surface.
The only egress is HTTPS to the firewall management plane.

---

## 8. Developer Internals

### Repository layout

```text
pan-os-python/
+-- panos/
|   +-- __init__.py        Module-level helpers, PanOSVersion, getlogger
|   +-- base.py            PanObject, VersionedPanObject, PanDevice, XapiWrapper
|   +-- errors.py          22 exception classes
|   +-- firewall.py        Firewall, FirewallState, FirewallCommit
|   +-- panorama.py        Panorama, DeviceGroup, Template, TemplateStack, ...
|   +-- policies.py        Rulebase, SecurityRule, NatRule, hit count, audit comments
|   +-- objects.py         Address/Service/Application objects, Tag, EDL, ...
|   +-- network.py         Zones, interfaces, routers, OSPF/BGP/RIP, IPsec/IKE
|   +-- device.py          Vsys, SystemSettings, NTP, server profiles, certs, admins
|   +-- ha.py              HighAvailability, HA1/2/3
|   +-- predefined.py      Predefined applications/services/tags
|   +-- updater.py         SoftwareUpdater, ContentUpdater
|   +-- userid.py          User-ID subsystem
|   +-- plugins.py         CloudServicesPlugin, RemoteNetwork, Prisma Access
+-- examples/
|   +-- bulk_address_objects.py
|   +-- bulk_subinterfaces.py
|   +-- dyn_address_group.py
|   +-- ensure_security_rule.py
|   +-- log_forwarding_profile.py
|   +-- prisma_access_create_remote_network.py
|   +-- prisma_access_list_RN_regions_bw.py
|   +-- prisma_access_show_jobs_status.py
|   +-- prisma_access_show_remote_net_per_tenant.py
|   +-- upgrade.py
|   +-- userid.py
+-- tests/
|   +-- test_*.py            Unit tests (pytest)
|   +-- live/
|       +-- conftest.py      Fixtures: fw, dev, pano, pairing
|       +-- testlib.py       FwFlow, DevFlow, PanoFlow base test classes
|       +-- test_device_config.py
|       +-- test_network.py
|       +-- test_objects.py
|       +-- test_set_reference.py
|       +-- test_userid.py
+-- docs/                   Sphinx config + module-*.rst pages
+-- pyproject.toml
+-- requirements.txt        Pins pan-python==0.17.0 with SHA-256 hashes
+-- tox.ini
+-- .flake8 / .isort.cfg / mypy.ini / .bandit
+-- .releaserc.json         Semantic-release config
```

### The metaclass / object-tree machinery

Three machinery classes in `panos/base.py` run the show:

- **`PanObject`** -- root class. Implements the in-memory tree
  (`add`, `insert`, `extend`, `pop`, `remove`, `find`, `findall`),
  XPath construction (`xpath`, `xpath_short`, `xpath_root`), and the
  basic CRUD verbs (`apply`, `create`, `delete`, `refresh`).
- **`VersionedPanObject(PanObject)`** -- adds version awareness via
  `_xpaths` (a `ParentAwareXpath`) and `_params` (a tuple of
  `VersionedParamPath`). The `__getattr__` / `__setattr__` overrides
  proxy attribute access through the version profile so
  `rule.action = "deny"` writes through to the right XML element for
  the device's reported PAN-OS version.
- **`VersioningSupport`** -- backing data structure for
  `_xpaths`/`_params`. Each entry is registered for a minimum PAN-OS
  version; lookup picks the highest version <= device version.

The classic shape of a `VersionedPanObject` subclass:

```python
class AddressObject(VersionedPanObject):
    ROOT = Root.VSYS
    SUFFIX = ENTRY

    def _setup(self):
        self._xpaths.add_profile(value="/address")
        params = []
        params.append(VersionedParamPath("value", path="{type}"))
        params.append(VersionedParamPath(
            "type", default="ip-netmask",
            values=["ip-netmask", "ip-range", "ip-wildcard", "fqdn"],
            path="{type}",
        ))
        params.append(VersionedParamPath("description", path="description"))
        params.append(VersionedParamPath("tag", path="tag", vartype="member"))
        self._params = tuple(params)
```

To add a parameter that exists only on PAN-OS 11.0+:

```python
params.append(VersionedParamPath("new_field", exclude=True))
params[-1].add_profile("11.0.0", path="new-field")
```

The `exclude=True` profile is the "doesn't exist before" marker; it
prevents the SDK from sending the element on older devices.

### Refresh strategies

`refresh()` (per-instance):

1. Constructs the XPath for this object.
2. Calls `xapi.get(xpath)` (or `xapi.show(xpath)` if
   `running_config=True`).
3. Walks the response, calls `parse_xml(elm)` on this object, then
   recursively on all CHILDTYPES if `refresh_children=True`.
4. Replaces the in-memory params from the response.

`refreshall(parent, ...)` (class method):

1. Computes the parent's XPath plus this class's relative XPath.
2. Calls `xapi.get` for the whole list.
3. Iterates entries, calls `refreshall_from_xml(...)` to build new
   instances of the class, attaches each to `parent` (if `add=True`).

Two performance levers: `name_only=True` (entry names only, no
params), and `running_config=True` (running config XPath; sometimes
faster on large candidates).

### Exception hierarchy

Rooted at `pan.xapi.PanXapiError` (third-party):

```text
PanXapiError                       (from pan.xapi)
    PanDeviceError                 (from panos.errors)
        PanDeviceXapiError
            PanInvalidCredentials
            PanURLError
            PanConnectionTimeout
            PanCommitInProgress
            PanInstallInProgress
            PanCommitFailed                # has .result
            PanCommitNotNeeded
            PanSessionTimedOut
            PanNoSuchNode
            PanObjectMissing
            PanHAConfigSyncFailed
            PanHASyncInProgress
        PanJobTimeout
        PanLockError
        PanPendingChanges
        PanDeviceNotSet
        PanNotConnectedOnPanorama
        PanNotAttachedOnPanorama
        PanObjectError
        PanApiKeyNotSet
        PanActivateFeatureAuthCodeError
        PanOutdatedSslError
```

22 leaf classes total (per `panos/errors.py` and the class hierarchy
extraction). `PanCommitFailed` is the most useful one to catch
explicitly -- it carries `.result` with the validator messages.

### Wrapping `pan.xapi.PanXapi`

`XapiWrapper(pan.xapi.PanXapi)` (in `panos/base.py`) is the integration
point:

- `make_method(super_method_name, super_method)` -- generates wrapper
  methods at class-creation time that call `super_method` and route
  any raised `PanXapiError` through `classify_exception()`.
- `classify_exception(e)` -- pattern-matches the exception text/type
  and re-raises a more specific `Pan*Error` subclass.

This is why every API call you make from your script can raise either
the generic `PanXapiError` or one of the SDK's specific subclasses.

### Testing strategy

| Tier | Where | Runs against |
|---|---|---|
| Unit | `tests/test_*.py` | Fixtures only; no network |
| Standards | `tests/test_standards.py` | Lint-style class structure checks |
| Versioning | `tests/test_versioning.py` | `PanOSVersion` ordering correctness |
| Live | `tests/live/test_*.py` | A real firewall + Panorama (CI fixture) |

Live tests use the `FwFlow`/`DevFlow`/`PanoFlow` base classes
(`tests/live/testlib.py`), which provide a 5-step lifecycle:
`setup_dependencies` -> `create` -> `refreshall` -> `update` ->
`cleanup`. The pattern is worth studying if you write scripts that
need to be idempotent against live devices.

### Adding a new class

The standard recipe to add a new firewall config type:

1. Subclass `VersionedPanObject` in the appropriate module
   (`policies.py` for a rule, `objects.py` for an object,
   `network.py` for an interface, etc.).
2. Set `ROOT`, `SUFFIX`, `XPATH`/`_xpaths`, and `_params` in `_setup()`.
3. Add it to the parent's `CHILDTYPES` (e.g., `Firewall.CHILDTYPES`,
   `DeviceGroup.CHILDTYPES`, `Rulebase.CHILDTYPES`).
4. Add a unit test in `tests/test_*.py`.
5. Add a live test (subclass of `FwFlow` in `tests/live/test_*.py`)
   that exercises create/refresh/update/delete.

`tests/test_classic_objects.py`, `tests/test_panorama.py`, and
`tests/test_userid.py` are good reference reads.

### Contributing

The repo follows conventional commits, semantic-release, Black-style
formatting, isort, mypy, flake8, and bandit. CI runs the unit test
matrix via `tox`. The live test suite requires a working firewall and
Panorama -- it's run manually, not on every PR.

---

## 9. Integration Points

### What `pan-os-python` calls into

| Target | Purpose | How |
|---|---|---|
| Firewall mgmt plane | All config + op operations | HTTPS via `pan.xapi.PanXapi` |
| Panorama mgmt plane | Same, plus device-group / template / commit-all | HTTPS via `pan.xapi.PanXapi` |

That's it. No external services, no registries, no databases.

### What might call into `pan-os-python`

| Caller | Pattern |
|---|---|
| Cron / systemd timer | Run a script on a schedule |
| CI/CD (GitHub Actions, GitLab CI) | Run on PR merge to push config |
| Workflow engines (Airflow, Argo, Temporal) | Multi-step orchestration |
| Web UI / Flask / FastAPI app | Wrap the SDK behind a REST endpoint for non-Python consumers |
| Ansible | The companion `paloaltonetworks.panos` Ansible collection wraps this SDK |
| Terraform | Not directly -- use the dedicated Terraform provider |
| Skyline-style monitors | Call `op()` repeatedly to scrape metrics |

> [!TIP]
> If you're integrating with Ansible, prefer the official
> `paloaltonetworks.panos` collection, which uses this SDK under the
> hood and exposes idempotent module wrappers. Don't re-implement
> idempotency in your playbooks.

### Plugin ecosystem (Prisma Access / Cloud Services)

`panos.plugins` provides typed objects for the Cloud Services /
Prisma Access plugin:

- `CloudServicesPlugin` -- root attached to a `Panorama`.
- `RemoteNetworks`, `RemoteNetwork` -- remote network sites.
- `AggBandwidth`, `Region`, `Link` -- bandwidth allocation primitives.
- `Tenants`, `AccessDomain` -- multi-tenant support.
- `InternalDnsMatch`, `PrimaryInternalDNSServer`,
  `SecondaryInternalDNSServer`, `PrimaryPublicDNSServer`,
  `SecondaryPublicDNSServer`.
- `Bgp`, `BgpPeer`, `RoutingPreference`.

The five `examples/prisma_access_*.py` scripts demonstrate end-to-end
flows: create a remote network with IKE/IPsec, list regions and
bandwidth, show jobs, walk multi-tenant deployments.

### User-ID integration

`fw.userid.*` is the Firewall-only User-ID API:

- **User-IP mappings**: `login`, `logins`, `logout`, `logouts`.
- **IP-tag mappings (DAGs)**: `register`, `unregister`,
  `get_registered_ip`, `clear_registered_ip`, `audit_registered_ip`,
  `audit_registered_ip_for_tag`.
- **User groups**: `set_group`, `get_groups`, `get_group_members`.
- **User tags (Dynamic User Groups)**: `tag_user`, `untag_user`,
  `get_user_tags`.
- **Batching**: `batch_start`, `batch_end`, `send` -- accumulate
  multiple ops into one UID message.

> [!NOTE]
> Use `batch_start()` / `batch_end()` if you're scripting bulk SOAR
> responses (block 100 IPs at once). One UID message instead of 100
> sequential calls.

### Predefined-objects integration

`device.predefined` provides typed access to PAN-OS-shipped objects:

- `refresh_application(name)`, `refresh_service(name)`,
  `refresh_tag(name)` -- pull a single predefined entry.
- `refreshall_applications()`, `refreshall_services()`,
  `refreshall_tags()`, `refreshall()` -- pull everything.
- `application(name, refresh_if_none=True, include_containers=True)`,
  `service(name)`, `tag(name)` -- accessor returning the cached
  `ApplicationObject`/`ServiceObject`/`Tag` after refresh-if-needed.

This is useful for validation: before pushing a `SecurityRule` that
references `application=["chrome-base"]`, verify the predefined app
exists on the target version.

### Software / content updates

`device.software` (`SoftwareUpdater`) and `device.content`
(`ContentUpdater`) wrap the device's update subsystem:

- **Software**: `info()`, `check()`, `download(version)`,
  `install(version)`, `download_install(version)`,
  `download_install_reboot(version)`,
  `upgrade_to_version(target_version, dryrun)`. The latter handles
  intermediate hops automatically (e.g., 9.1 -> 10.1 -> 11.1).
- **Content**: `info()`, `check()`, `download(sync_to_peer, sync)`,
  `install(version, ...)`, `download_and_install_latest(sync)`,
  `downgrade(sync)`.

> [!CAUTION]
> Content updates can change application identification and break
> existing rules unintentionally. Test in a lab vsys first, schedule
> production updates during a maintenance window.

### CI/CD example

A minimal GitHub Actions step that pushes a security rule on PR
merge:

```yaml
- name: Push rule to firewall
  env:
    PAN_HOSTNAME: ${{ secrets.PAN_HOSTNAME }}
    PAN_API_KEY:  ${{ secrets.PAN_API_KEY }}
  run: |
    pip install pan-os-python==1.12.1
    python scripts/ensure_rule.py
```

Pair with branch protection, required reviewers, and a "dry run"
script that calls `refreshall` and prints diffs without committing.

---

_Generated April 2026 from PaloAltoNetworks/pan-os-python at 641489d. 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._
