# pan-os-python User Guide

`pan-os-python` is the official Palo Alto Networks SDK for Python that wraps
the PAN-OS XML API in a typed, parent-aware object model. Instead of crafting
XPath strings or XML payloads, you compose a Python object tree
(`Firewall` -> `Rulebase` -> `SecurityRule`, or `Panorama` -> `DeviceGroup` ->
`AddressObject`) and the SDK translates `.create()`, `.apply()`, `.refresh()`,
and `.delete()` into the right `type=set`, `type=edit`, `type=get`, and
`type=delete` API calls. Bulk operations, version-aware schemas, vsys
handling, and HA-peer awareness are all built in.

This guide is for SREs, network automation engineers, and platform
engineers who write Python tools to manage Next-Generation Firewalls and
Panorama at scale. It assumes you can reach the firewall management plane
on TCP/443 and have an admin account (or a pre-issued API key).

> [!NOTE]
> The companion **Administrator & Developer Guide** covers the architecture
> of the object tree, every constructor parameter for the major classes,
> the exception hierarchy, security hardening (TLS, key handling,
> exported configurations), and the internal metaclass machinery used by
> `VersionedPanObject`.

---

## 1. Quick Start

Get from zero to a verified API call in under two minutes.

### Install the SDK

```bash
pip install pan-os-python
```

That single command pulls in `pan-os-python` and its only runtime
dependency, `pan-python==0.17.0`, from PyPI.

### Five-line connect-and-fetch script

Save the following as `hello_panos.py`, replacing the host, username, and
password with your own:

```python
from panos.firewall import Firewall

fw = Firewall("192.0.2.10", "admin", "your-password")
info = fw.refresh_system_info()
print(info)
```

Run it:

```bash
python hello_panos.py
```

Expected output (truncated):

```text
SystemInfo(version='11.1.2', platform='PA-VM', serial='007012345678901')
```

The first method that touches the device (`refresh_system_info()` here)
makes the SDK call `keygen` under the hood, cache the API key on the
`Firewall` object, and pull `<show><system><info>` from the management
plane. After this call, `fw.version`, `fw.platform`, and `fw.serial` are
populated.

> [!TIP]
> Once `fw.api_key` is set, you can store it somewhere safe and reuse it on
> the next run instead of sending the password again. See
> **Section 5: Common Tasks** for the recommended auth pattern.

### What just happened

1. `Firewall("192.0.2.10", "admin", "...")` constructs the device root of an
   object tree. No network I/O yet.
2. `fw.refresh_system_info()` issued a `keygen` call (because no `api_key=`
   was passed), then ran `show system info`, and parsed the XML into the
   `SystemInfo` named tuple.
3. The SDK populated `fw.version`, `fw.platform`, `fw.serial`, and
   `fw._version_info` so subsequent calls know which version-specific
   schema to send.

### Where to go next

- **Section 2** for every supported install method, including dev
  installs and pinning notes.
- **Section 4** for the device-tree paradigm (`add()`, `find()`,
  `findall()`, `refresh()`, `apply()`, `delete()`, `create()`).
- **Section 5** for twelve real-world recipes (bulk address objects,
  security rules, Panorama device groups, hit counts, software
  upgrade, User-ID).

---

## 2. Installation

### Supported environments

| Item | Supported |
|---|---|
| Python | 3.6 and later (the package emits a `RuntimeWarning` on older versions) |
| Operating systems | Linux, macOS, Windows -- anywhere CPython runs |
| Firewall versions | PAN-OS 6.1 through 11.x (per-class version profiles handle schema differences) |
| Panorama versions | Same range -- `Panorama` is a peer device class to `Firewall` |
| Network | TCP/443 to the firewall/Panorama management interface |
| Credentials | Local admin account, or pre-issued API key from `keygen` |

> [!IMPORTANT]
> PAN-OS exposes the management API on the management plane only, on
> TCP/443 by default. If your firewall lives behind a jump host, run your
> Python script from the jump host or set up a forwarded tunnel. The SDK
> does not ship a SOCKS or HTTP-proxy helper.

### Install methods

=== "pip (recommended)"
    ```bash
    pip install pan-os-python
    ```

=== "Pinned version"
    ```bash
    pip install "pan-os-python==1.12.1"
    ```

=== "Inside a virtualenv"
    ```bash
    python -m venv .venv
    source .venv/bin/activate           # macOS/Linux
    # .venv\Scripts\activate            # Windows PowerShell
    pip install pan-os-python
    ```

=== "Poetry"
    ```bash
    poetry add pan-os-python
    ```

=== "Editable / dev install"
    ```bash
    git clone https://github.com/PaloAltoNetworks/pan-os-python.git
    cd pan-os-python
    pip install -e .
    ```

> [!TIP]
> Use a virtualenv (or `poetry`/`pipx`) for every project. The SDK pulls in
> `pan-python==0.17.0` as a hard pin; mixing it with conflicting versions
> of `pan-python` already on the system path is the most common
> first-day-on-the-job failure mode.

### Verify the install

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

```text
1.12.1
```

You can also import each module to confirm the package is on the path:

```python
import panos
import panos.firewall
import panos.panorama
import panos.objects
import panos.policies
import panos.network
import panos.device
import panos.errors
print("ok")
```

### Python 3.12+ note

The SDK historically imports `LooseVersion` from `distutils`, which was
removed from the Python standard library in 3.12. Community-tracked issues
**#580** and **#529** call this out. If you see
`ModuleNotFoundError: No module named 'distutils'`, either pin to
Python 3.11 or install the latest published patch release that addresses
it.

> [!WARNING]
> Do not run `pip install pan-os-python` as root. Use a virtualenv or the
> `--user` flag. Installing system-wide on a shared host can collide with
> distribution-managed Python packages.

---

## 3. Configuration

`pan-os-python` is a library, not a daemon -- there are no config files to
edit. Configuration happens in three places:

1. **Constructor arguments** -- the `hostname`, `api_username`,
   `api_password`, `api_key`, `port`, `vsys`, `serial`, `is_virtual`,
   `timeout`, and `interval` you pass to `Firewall(...)` or `Panorama(...)`.
2. **Environment variables** -- your scripts may read them (the bundled
   examples use `PAN_HOSTNAME`, `PAN_USERNAME`, `PAN_PASSWORD`,
   `PAN_LOG_PROFILE`), but the SDK itself does not auto-load any.
3. **Python `logging`** -- the SDK uses the standard `logging` module with
   four extra debug levels (`DEBUG1`-`DEBUG4`) for ever-deeper API tracing.

### Connection parameters

```python
from panos.firewall import Firewall

fw = Firewall(
    hostname="fw01.example.net",   # required
    api_username="automation",     # paired with api_password
    api_password="REDACTED",       # used once to fetch an api_key
    api_key=None,                  # OR pass an existing key to skip keygen
    port=443,                      # management plane TCP port
    vsys="vsys1",                  # default vsys for vsys-rooted objects
    serial=None,                   # set when proxying via Panorama target=
    is_virtual=None,               # True for VM-Series; auto-detected
    timeout=1200,                  # seconds for async jobs (commit, etc.)
    interval=0.5,                  # poll interval for async jobs
)
```

The same parameter list applies to `Panorama(...)` minus `serial`,
`vsys`, `is_virtual`. See the Admin Guide **Section 3** for every
documented argument across both classes.

### Environment-variable pattern (recommended)

The bundled example scripts (`examples/log_forwarding_profile.py`,
`examples/prisma_access_create_remote_network.py`) use this idiom -- copy
it into your own automation:

```python
import os
from panos.firewall import Firewall

fw = Firewall(
    os.environ["PAN_HOSTNAME"],
    os.environ["PAN_USERNAME"],
    os.environ["PAN_PASSWORD"],
)
```

Then:

```bash
export PAN_HOSTNAME=fw01.example.net
export PAN_USERNAME=automation
export PAN_PASSWORD=...           # or use a secret manager wrapper
python my_script.py
```

> [!IMPORTANT]
> Do not check secrets into source control. Pull them from a secret
> manager (Vault, AWS SSM, Azure Key Vault, 1Password, etc.) and inject
> them as environment variables for the duration of the script. The SDK
> never persists credentials to disk on its own.

### Logging configuration

Standard Python logging works:

```python
import logging
logging.basicConfig(
    format="%(levelname)s:%(name)s:%(message)s",
    level=logging.DEBUG,
)
```

For deeper protocol tracing, use the SDK's extra debug levels:

```python
import logging
import panos
logging.basicConfig(level=panos.DEBUG2)   # DEBUG2, DEBUG3, DEBUG4 also exist
```

`DEBUG1` shows method entry/exit. `DEBUG2`/`DEBUG3` show XML payloads.
`DEBUG4` is the noisiest. Drop the level before shipping logs to a SIEM.

> [!CAUTION]
> The deepest debug levels (`DEBUG3`/`DEBUG4`) print full XML payloads,
> which include candidate-config snippets -- those snippets contain
> credentials, certificate material, and pre-shared keys. Capture
> `DEBUG3+` only to a local file you control.

---

## 4. Core Usage

The single most important concept in `pan-os-python` is the **object tree**.
You build a Python tree that mirrors the firewall's XML config, and a small
set of methods on every node moves data between your tree and the live
device.

### The device tree

```text
PanDevice (abstract)
   |
   +-- Firewall (root for a NGFW or vsys)
   |      |
   |      +-- device.Vsys
   |      |     +-- objects.AddressObject, AddressGroup, ServiceObject ...
   |      |     +-- policies.Rulebase
   |      |             +-- policies.SecurityRule
   |      |             +-- policies.NatRule
   |      |             +-- policies.DecryptionRule
   |      |             +-- policies.AuthenticationRule
   |      |             +-- policies.ApplicationOverride
   |      |             +-- policies.PolicyBasedForwarding
   |      |     +-- network.Zone, EthernetInterface, Layer3Subinterface ...
   |      +-- ha.HighAvailability
   |      +-- objects.* (when not multi-vsys)
   |      +-- policies.Rulebase
   |
   +-- Panorama (root for a Panorama appliance)
          |
          +-- panorama.DeviceGroup
          |     +-- firewall.Firewall (managed devices)
          |     +-- objects.* (shared at DG scope)
          |     +-- policies.PreRulebase / PostRulebase
          |             +-- policies.SecurityRule, NatRule ...
          +-- panorama.Template
          |     +-- network.*
          |     +-- device.Vsys, SystemSettings ...
          +-- panorama.TemplateStack
          +-- objects.* (Panorama "Shared")
```

The exact `CHILDTYPES` for `Firewall`, `Panorama`, `DeviceGroup`,
`Template`, and `TemplateStack` are enumerated as tuples on the class
itself (see `panos/firewall.py` `Firewall.CHILDTYPES`, `panos/panorama.py`
`DeviceGroup.CHILDTYPES`). Every parent advertises the legal child types
in that tuple.

> [!NOTE]
> You don't pre-declare the whole tree. You attach only what you need.
> A script that lists security rules creates `Firewall -> Rulebase` and
> walks the tree from there; it never instantiates a `Vsys`, `Zone`, or
> `Interface`.

### The five core methods

Every object derived from `PanObject` (which is everything you build) has
the same vocabulary:

| Method | What it does on the live device | Source |
|---|---|---|
| `add(child)` | **No I/O.** Attach `child` in your local tree under this object. Returns the child for chaining. | `panos.base.PanObject.add` |
| `create()` | Issue API `type=set`. Add this node and its children to the running config; safe to repeat. | `panos.base.VersionedPanObject.create` |
| `apply()` | Issue API `type=edit`. **Replace** this node's subtree with what you have in memory. Destructive if your tree is incomplete. | `panos.base.VersionedPanObject.apply` |
| `refresh(running_config=False, refresh_children=True, exceptions=True, xml=None)` | Issue API `type=get` (or `type=show` if `running_config=True`); replace this node's params from the device. | `panos.base.PanObject.refresh` |
| `delete()` | Issue API `type=delete` and detach from the local tree. | `panos.base.PanObject.delete` |

And four navigation/lookup helpers:

| Method | What it does |
|---|---|
| `find(name, class_type=None, recursive=False)` | Return the first matching child by name (and optionally class). |
| `findall(class_type, recursive=False)` | Return all children of a given class. |
| `find_or_create(name, class_type, ...)` | Return existing child or instantiate-and-add a new one. |
| `findall_or_create(class_type, ...)` | Same idea for collections. |
| `refreshall(parent, running_config=False, add=True, exceptions=False, name_only=False)` | **Class method.** Pull every instance of this class from the device under `parent` into the local tree. |

> [!IMPORTANT]
> `apply()` replaces, `create()` adds. `apply()` does API `type=edit` --
> the firewall's interpretation is "replace this node with what I just
> sent you". If your local tree has only the node you changed but not its
> siblings, calling `apply()` on the parent silently truncates the
> others. When in doubt, use `create()`.

### Anatomy of a typical script

Almost every workflow follows the same five steps:

1. **Construct the device root** -- `Firewall(...)` or `Panorama(...)`.
   No I/O.
2. **Attach the children you need** -- `fw.add(Rulebase())`,
   `pano.add(DeviceGroup(name="DG1"))`. Still no I/O.
3. **Pull current state** -- `SecurityRule.refreshall(rulebase)` or
   `obj.refresh()`.
4. **Mutate Python objects** in memory.
5. **Push back** -- `.create()` for new objects, `.apply()` to replace,
   `.delete()` to remove. Then `fw.commit(sync=True)` to persist.

The bundled `examples/ensure_security_rule.py` is a textbook example of
this shape: `Firewall` -> `Rulebase` -> `refreshall` -> check existence ->
`add` + `create` -> `commit(sync=True)`.

### Bulk operations: the `_similar` family

For performance, the SDK exposes batched siblings of the standard verbs.
These methods are defined on `PanObject` (`panos/base.py`):

| Method | Equivalent of | Effect |
|---|---|---|
| `create_similar()` | `create()` on every sibling of the same class | One API call instead of N |
| `apply_similar()` | `apply()` on every sibling of the same class | One API call instead of N |
| `delete_similar()` | `delete()` on every sibling of the same class | One API call instead of N |

> [!TIP]
> If you need to push hundreds of address objects, security rules, or
> subinterfaces, build them all in your tree first and call
> `create_similar()` on **any one** of them. The SDK collects the
> siblings and sends a single request. The bundled
> `examples/bulk_address_objects.py` demonstrates a 600-object create
> that completes in roughly the time of a single `create()`.

### Authentication: API key vs. username/password

Three patterns are supported:

1. **Username + password** -- pass `api_username` and `api_password` to
   the constructor. The SDK calls `keygen` on the first device-touching
   method and caches the key on the object.
2. **Pre-issued API key** -- pass `api_key=` to the constructor. The SDK
   skips `keygen` entirely. Get the key once with the username/password
   form, then read `fw.api_key` and persist it.
3. **Auto-detect device type** -- `PanDevice.create_from_device(host,
   user, pw)` connects, runs `show system info`, then returns either a
   `Firewall` or `Panorama` instance with the correct subclass.

```python
from panos.base import PanDevice

device = PanDevice.create_from_device("10.0.0.1", "admin", "your-password")
print(type(device).__name__)        # 'Firewall' or 'Panorama'
print(device.api_key)               # auto-fetched
```

> [!TIP]
> Reuse the API key. Calling `keygen` once a day is fine; calling it on
> every script run wastes ~200 ms and creates noise in the firewall's
> auth logs.

### XPath, vsys, and `op()`

Two escape hatches sit alongside the typed objects:

- **XPath helpers** -- every object exposes `obj.xpath()`,
  `obj.xpath_short()`, `obj.xpath_root(...)`, etc., which compute the
  correct XPath string for that object given its position in the tree.
  Use these when you need to craft a custom request.
- **`device.op(cmd, vsys=None, xml=False, cmd_xml=True, extra_qs=None,
  retry_on_peer=False, quote=None)`** -- run an arbitrary op-command. Pass
  CLI text and let the SDK translate to XML, or pass an XML element tree
  with `cmd_xml=False`.

```python
resp = fw.op("show clock")              # CLI form, SDK builds XML
resp = fw.op("<show><clock/></show>",   # XML form, you build it
            cmd_xml=False)
```

`op()` returns the parsed XML response as an `ElementTree.Element`.

---

## 5. Common Tasks

Twelve recipes drawn from the bundled `examples/` directory and the live
test suite (`tests/live/`). Each follows the same shape: Goal,
Prerequisites, Steps, Verify, If it fails.

### Recipe 1 -- Connect to a firewall and dump the running config tree

**Goal.** Confirm credentials, fetch `show system info`, and walk the
top-level subtree.

**Prerequisites.** Network reachability to TCP/443, an admin account.

**Steps.**

```python
from panos.firewall import Firewall
from panos.objects import AddressObject

fw = Firewall("10.0.0.1", "admin", "password")

# First device-touching call: triggers keygen + show system info
print(fw.refresh_system_info())

# Pull every address object into the local tree
addresses = AddressObject.refreshall(fw)
print(f"{len(addresses)} address objects found")

# Walk and print
for ao in addresses:
    print(ao.name, ao.value, ao.type, ao.description)
```

**Verify.** You should see one `print` per address object, plus the
`SystemInfo` tuple. `fw.api_key` is now set, `fw.version` is populated.

**If it fails.** A `PanInvalidCredentials` means username/password
rejected. A `PanURLError` means the host is unreachable or TLS is
failing. See **Section 6: Troubleshooting**.

### Recipe 2 -- Pull a Panorama device-group's address objects

**Goal.** From Panorama, walk into a specific device-group and list its
local address objects (i.e., not "Shared").

**Steps.**

```python
from panos.panorama import Panorama, DeviceGroup
from panos.objects import AddressObject

pano = Panorama("panorama01.example.net", "admin", "password")

dg = pano.add(DeviceGroup(name="Branch-Offices"))
addresses = AddressObject.refreshall(dg)

for ao in addresses:
    print(f"{ao.name:30}  {ao.type:12}  {ao.value}")
```

**Verify.** Address objects scoped to that DG are printed. Shared
addresses (at the Panorama root) are **not** included -- those live
under `pano`, not under `dg`.

**If it fails.** If the DG name is wrong you'll see `PanObjectMissing`.
If the admin account lacks DG visibility, the list will be silently
empty.

### Recipe 3 -- Bulk-create address objects from a CSV

**Goal.** Read a `name,value,type,description` CSV and push hundreds of
objects in one API call.

**Prerequisites.** Working `Firewall` connection; the file
`addresses.csv` next to your script.

**Steps.**

```python
import csv
from panos.firewall import Firewall
from panos.objects import AddressObject

fw = Firewall("10.0.0.1", "admin", "password")

new = []
with open("addresses.csv") as f:
    for row in csv.DictReader(f):
        ao = AddressObject(
            name=row["name"],
            value=row["value"],
            type=row.get("type", "ip-netmask"),
            description=row.get("description", ""),
        )
        fw.add(ao)
        new.append(ao)

# One API call for the whole batch:
new[0].create_similar()

fw.commit(sync=True)
print(f"Created {len(new)} address objects")
```

**Verify.** `fw.commit(sync=True)` returns a dict whose `success` is
`True`. Browse the firewall GUI under **Objects -> Addresses** and
confirm the entries.

**If it fails.** Ground-truth common failures are listed in
**Section 6**; see *Schema error on create_similar* for the
most-frequent one.

> [!WARNING]
> `create_similar()` does NOT do a "create or update" -- duplicate names
> will error out. Pre-flight with
> `AddressObject.refreshall(fw, add=False)` and filter your input list
> against the existing names.

### Recipe 4 -- Add a security rule and commit

**Goal.** Idempotently ensure a single security rule exists; commit when
you add it.

**Steps.** Mirror of `examples/ensure_security_rule.py`:

```python
from panos.firewall import Firewall
from panos.policies import Rulebase, SecurityRule

DESIRED = {
    "name": "Block ssh outbound",
    "description": "Prevent ssh usage",
    "fromzone": "any",
    "tozone": "any",
    "application": "ssh",
    "action": "deny",
    "log_end": True,
}

fw = Firewall("10.0.0.1", "admin", "password")
rulebase = fw.add(Rulebase())

current = SecurityRule.refreshall(rulebase)
if any(r.name == DESIRED["name"] for r in current):
    print("Already present; nothing to do.")
else:
    new_rule = SecurityRule(**DESIRED)
    rulebase.add(new_rule)
    new_rule.create()
    print("Created. Committing...")
    fw.commit(sync=True)
    print("Done.")
```

**Verify.** Re-run the script -- the second run should print "Already
present". The firewall GUI shows the rule under
**Policies -> Security**.

**If it fails.** A `PanCommitFailed` indicates a config validation
failure. Issue **#592** ("schema error despite valid structure") is the
canonical reference for that failure mode -- usually a missing `tozone`
or `application` value.

### Recipe 5 -- Compare candidate vs. running config

**Goal.** See whether anything is pending an admin commit.

**Steps.**

```python
from panos.firewall import Firewall

fw = Firewall("10.0.0.1", "admin", "password")
print("Pending changes:", fw.pending_changes())
```

The SDK's `pending_changes()` issues `<check><pending-changes/></check>`
and returns a boolean.

For a deeper diff, refresh objects against `running_config=True` and
against the candidate, then compare in Python:

```python
from panos.objects import AddressObject

candidate = {ao.name: ao for ao in AddressObject.refreshall(fw, add=False)}
running = {ao.name: ao for ao in AddressObject.refreshall(
    fw, add=False, running_config=True)}

added = candidate.keys() - running.keys()
removed = running.keys() - candidate.keys()
print("In candidate, not running:", added)
print("In running, not candidate:", removed)
```

> [!NOTE]
> `refresh(running_config=True)` and `refreshall(..., running_config=True)`
> hit the running-config XPath instead of the candidate. Most other API
> calls default to candidate.

### Recipe 6 -- Push from Panorama to managed devices

**Goal.** From a Panorama object, commit-all to a specific device-group
and optionally wait for the push jobs to finish.

**Steps.**

```python
from panos.panorama import Panorama

pano = Panorama("panorama01.example.net", "admin", "password")
job_id_or_result = pano.commit_all(
    sync=True,                   # wait for the local commit
    sync_all=True,               # also wait for downstream device jobs
    devicegroup="Branch-Offices",
    description="Daily push 2026-04-28",
    include_template=True,
)
print(job_id_or_result)
```

`Panorama.commit_all(...)` accepts `sync`, `sync_all`, `exception`,
`devicegroup`, `serials`, `cmd`, `description`, `include_template`, plus
template-stack and log-collector controls.

**Verify.** A successful synchronous push returns a dict with
`success=True`. Asynchronous calls return the job ID; poll with
`pano.syncjob(job_id, sync_all=True)`.

**If it fails.** See issues **#574** ("socket.timeout when submitting
push") and **#324** ("improve feedback after failed Panorama commit
jobs"). Set `exception=True` to make commit failures raise
`PanCommitFailed`.

### Recipe 7 -- Refresh and walk a Panorama hierarchy

**Goal.** Print the device-group hierarchy as a tree.

**Steps.**

```python
from panos.panorama import Panorama

pano = Panorama("panorama01", "admin", "password")
hierarchy = pano.opstate.dg_hierarchy.fetch()
# hierarchy is dict: {"DG-Name": parent_or_None, ...}

children = {}
for child, parent in hierarchy.items():
    children.setdefault(parent, []).append(child)

def walk(parent, depth=0):
    for c in sorted(children.get(parent, [])):
        print("  " * depth + c)
        walk(c, depth + 1)

walk(None)
```

**Verify.** Output is the indented tree of device groups. Top-level
groups have parent `None`.

### Recipe 8 -- Auth options and key persistence

**Goal.** Move from "username/password every run" to "issue once, reuse
the key".

**Steps.**

```python
# Run once, manually, against the firewall:
from panos.firewall import Firewall
fw = Firewall("10.0.0.1", "admin", "password")
fw.refresh_system_info()
print(fw.api_key)        # save this somewhere safe
```

Then in production scripts:

```python
import os
from panos.firewall import Firewall

fw = Firewall(
    hostname=os.environ["PAN_HOSTNAME"],
    api_key=os.environ["PAN_API_KEY"],
)
fw.refresh_system_info()
```

> [!CAUTION]
> An API key is functionally a long-lived credential. Treat it like a
> password. Store in a secret manager, scope to a least-privilege admin
> role on the firewall, and rotate on a schedule.

### Recipe 9 -- Fetch system info, interfaces, and sessions for monitoring

**Goal.** Build a small monitoring snapshot.

**Steps.**

```python
from panos.firewall import Firewall

fw = Firewall("10.0.0.1", api_key="...")

# show system info
print(fw.refresh_system_info())

# show system resources (CPU/mem/swap) -- Firewall-only
print(fw.show_system_resources())

# Run any op-command and parse the XML response
xml = fw.op("show interface all")
for entry in xml.findall(".//ifnet/entry"):
    print(entry.findtext("name"), entry.findtext("state"))

# Active sessions
sessions = fw.op("show session info")
print(sessions.findtext(".//num-active"))
```

**Verify.** Each call prints a numeric or descriptive value. Run on a
schedule and pipe into your TSDB.

### Recipe 10 -- Enumerate / filter rules by tag, zone, or enabled flag

**Goal.** Print all `enabled` security rules that carry the tag
`internet`.

**Steps.**

```python
from panos.firewall import Firewall
from panos.policies import Rulebase, SecurityRule

fw = Firewall("10.0.0.1", api_key="...")
rb = fw.add(Rulebase())
rules = SecurityRule.refreshall(rb)

for r in rules:
    if r.disabled:
        continue
    if r.tag and "internet" in r.tag:
        print(f"{r.name:40}  {r.fromzone} -> {r.tozone}  ({r.action})")
```

**Verify.** Output is a filtered list. Cross-reference against the GUI's
**Policies -> Security** tag filter.

### Recipe 11 -- Use User-ID to register a tag against an IP

**Goal.** Tag IP `10.1.2.3` as `compromised` so a Dynamic Address Group
picks it up.

**Steps.**

```python
from panos.firewall import Firewall

fw = Firewall("10.0.0.1", api_key="...")
fw.userid.register("10.1.2.3", ["compromised"], timeout=3600)
```

The User-ID subsystem (`fw.userid`) is auto-attached to every
`PanDevice`. It exposes `.login()`, `.logins()`, `.logout()`,
`.logouts()`, `.register()`, `.unregister()`, `.get_registered_ip()`,
`.audit_registered_ip()`, `.tag_user()`, `.untag_user()`, `.set_group()`,
`.get_groups()`, `.get_group_members()`, `.get_user_tags()`, plus
`.batch_start()` / `.batch_end()` for batched UID messages.

**Verify.** Run `fw.userid.get_registered_ip()` and confirm the IP-tag
mapping. The DAG that filters on `compromised` will pick up the IP
within seconds.

> [!WARNING]
> User-ID is a Firewall-only API. Calling `.userid.*` on a `Panorama`
> instance is unsupported. The bundled `examples/dyn_address_group.py`
> shows the canonical pattern with `PanDevice.create_from_device()` so
> the same script works against either device type and exits gracefully
> when handed a Panorama.

### Recipe 12 -- Software upgrade

**Goal.** Upgrade a firewall to a target PAN-OS version, including all
intermediate hops.

**Steps.** The bundled `examples/upgrade.py` shows the canonical pattern.
The relevant SDK surface is `device.software` and its
`upgrade_to_version(target_version, dryrun=False)`:

```python
from panos.base import PanDevice

device = PanDevice.create_from_device("10.0.0.1", "admin", "password")
device.software.upgrade_to_version("11.1.2", dryrun=True)   # plan
device.software.upgrade_to_version("11.1.2")                # execute
```

`SoftwareUpdater` (auto-attached as `device.software`) exposes `info()`,
`check()`, `download(version, sync_to_peer, sync)`,
`install(version, load_config, sync)`,
`download_install(version, load_config, sync)`,
`download_install_reboot(version, load_config, sync)`, and
`upgrade_to_version(target_version, dryrun)`.

> [!CAUTION]
> Reboots interrupt traffic. Run `upgrade_to_version(...)` in a
> maintenance window, with HA configured for failover, and always
> pre-flight with `dryrun=True`. See open issue **#572** for an
> upgrade-flow regression worth knowing about.

---

## 6. Troubleshooting

The decision tree below walks you from "my script broke" to the right
section:

```text
Did the script ever connect?
  No  -> "PanURLError" / "PanConnectionTimeout" -> Network/TLS section
  Yes -> Authentication?
           "PanInvalidCredentials" -> Auth section
           "PanApiKeyNotSet"       -> Auth section
         Object not found?
           "PanObjectMissing"      -> Object/path section
           "PanNoSuchNode"         -> Object/path section
         Commit related?
           "PanCommitFailed"       -> Commit section
           "PanCommitInProgress"   -> Commit section
           "PanLockError"          -> Commit/lock section
         Panorama specific?
           "PanNotConnectedOnPanorama" / "PanNotAttachedOnPanorama"
                                   -> Panorama section
         Anything else            -> Generic section
```

### Network and TLS

#### Error: `PanURLError: certificate verify failed`

**Cause.** The firewall's management cert is self-signed or signed by a
CA your Python `ssl` module doesn't trust.

**Solution.**

1. **Preferred.** Install the firewall's CA cert into your trust store
   (`certifi.where()` location, OS keychain, or `REQUESTS_CA_BUNDLE`).
2. **Lab-only.** The supported approach is to install a corporate CA --
   the SDK does not ship a "skip TLS" toggle. If you absolutely must
   bypass for a one-off lab, monkey-patch the `pan.xapi` SSL context;
   the Admin Guide **Section 7** covers the exact knob and its risks.

**Prevention.** Use a real, trusted certificate on management
interfaces.

> [!CAUTION]
> Disabling TLS verification is a risk you accept across every script
> using that environment. Don't normalize it; install the cert.

#### Error: `PanConnectionTimeout`

**Cause.** TCP/443 to the firewall management interface is not
reachable within the timeout window.

**Solution.**

1. From the host running your script: `nc -zv <fw-ip> 443` or
   `curl -kvI https://<fw-ip>/`.
2. Confirm the management profile permits the source IP
   (**Device -> Setup -> Management -> Permitted IP Addresses**).
3. Increase the constructor `timeout=` if the device is just slow.

### Authentication

#### Error: `PanInvalidCredentials`

**Cause.** Username or password rejected by the firewall.

**Solution.** Validate via the GUI. Check whether the admin account is
locked, password-expired, or restricted to a specific source IP.

#### Error: `PanApiKeyNotSet`

**Cause.** A method that needs the live device was called before the
SDK could fetch an API key (e.g., you passed neither `api_key=` nor
`api_username` + `api_password`).

**Solution.** Provide one of the two credential modes in the
constructor.

### Object / path errors

#### Error: `PanObjectMissing`

**Cause.** `refresh()` (or a method that calls it) targeted an object
that doesn't exist on the device -- typically a typo in the name, the
wrong vsys, or the wrong device-group.

**Solution.** Use `refreshall(parent)` first to list what *does* exist,
then pick by exact name.

#### Error: `PanNoSuchNode`

**Cause.** The XPath the SDK constructed doesn't match anything on the
device -- usually a version-skew issue (you're using a class param that
existed in an older PAN-OS).

**Solution.** Check the class's `VersionedParamPath` profiles.
Cross-check your firewall version with `fw.version`.

> [!NOTE]
> Issue **#570** notes that PAN-OS 11.0+ no longer supports partial
> XPath queries the way older versions did. If you're hand-crafting
> XPaths (instead of letting the SDK build them), this can surface as
> `PanNoSuchNode`.

### Commit and locks

#### Error: `PanCommitFailed: Commit failed`

**Cause.** The commit was rejected by the firewall config validator.

**Solution.** The exception's `.result` attribute holds the raw
response; inspect `e.result` for the validator messages. Common
culprits: a security rule referencing an undefined object, a NAT rule
with conflicting source-translation params, or schema mismatches.
Issues **#592** and **#591** track common patterns.

```python
from panos.errors import PanCommitFailed
try:
    fw.commit(sync=True, exception=True)
except PanCommitFailed as e:
    print(e.result)
```

#### Error: `PanCommitInProgress`

**Cause.** Another admin (or process) holds an active commit.

**Solution.** Either wait, or call `fw.syncjob(job_id)` against the
in-flight job. To avoid the race, take a config lock:

```python
fw.add_config_lock(comment="upgrade script", exceptions=True)
try:
    ...                    # do work
    fw.commit(sync=True)
finally:
    fw.remove_config_lock()
```

#### Error: `PanLockError`

**Cause.** A commit or config lock is held by another admin.

**Solution.** `fw.check_commit_locks()` and `fw.check_config_locks()`
show who holds the lock. `fw.remove_all_locks()` (admin discretion)
clears them.

### Panorama-specific

#### Error: `PanNotConnectedOnPanorama` / `PanNotAttachedOnPanorama`

**Cause.** You instantiated a `Firewall` that's expected to be reached
through Panorama, but Panorama doesn't have it connected/attached.

**Solution.** Verify under **Panorama -> Managed Devices**. If the
device is expected to be managed: confirm its connectivity to Panorama
and the `devices` element of the device-group / template-stack.

#### Issue: only one address object returned (issue #567)

**Cause.** A bug pattern where `refreshall` returns a single object on a
firewall managed by Panorama, when many exist. Reported in issue
**#567**.

**Solution.** When in doubt, query Panorama directly for the device
group and refresh from there.

### Hit count and op-state

#### Issue: `cannot get opstate working for SecurityRule` (issue #594)

**Cause.** Hit-count requires PAN-OS 8.1+ and the rule must be in the
SDK tree under the matching rulebase type.

**Solution.** Refresh hit counts via the rulebase, not the rule:

```python
from panos.policies import Rulebase, SecurityRule

rb = fw.add(Rulebase())
SecurityRule.refreshall(rb)
results = rb.opstate.hit_count.refresh(style="security", all_rules=True)
for name, hc in results.items():
    print(name, hc.hit_count, hc.last_hit_timestamp)
```

#### Issue: `ApplicationObject's refreshall limits to 50 results` (issue #602)

**Cause.** PAN-OS API limit on certain object types in some versions.

**Solution.** Track the upstream issue; in the meantime, fetch in
batches or use `running_config=True` if your workflow allows.

### Other open issues worth knowing

| Issue | Symptom | Workaround |
|---|---|---|
| **#580** / **#529** | `ModuleNotFoundError: distutils` on Python 3.12+ | Pin to Python 3.11 until a release lands |
| **#598** | `hit_count` on Panorama pre/post rules returns nothing | Refresh from the matching rulebase, not the rule |
| **#586** | `refreshall` doesn't return rules pushed from Panorama | Refresh from Panorama side |
| **#501** | Modifier calls fail on `DeviceGroup` when a `Firewall` is a child | Detach the child Firewall before mutating |
| **#570** | Partial XPath unsupported in PAN-OS 11.0 | Use the full SDK-generated XPaths |

### When all else fails

Turn on `panos.DEBUG3` or `DEBUG4` logging and capture the actual XML
request/response. The issue tracker on GitHub (linked from the project
README) is the authoritative place to file bugs.

---

## 7. FAQ

**Q: Do I need to call `login()` before refreshing?**

No. Any method that touches the device automatically triggers a
`keygen` call if no `api_key` is set yet. After the first call, the
key is cached on the device object.

**Q: What's the difference between `create()` and `apply()`?**

`create()` does API `type=set` -- it adds without removing siblings.
`apply()` does API `type=edit` -- it **replaces** the node's subtree.
Always use `create()` for new objects. Use `apply()` only when you've
refreshed the full sibling set into your tree first.

**Q: How do I commit changes after creating an object?**

Call `fw.commit(sync=True)` (Firewall) or `pano.commit(sync=True)` /
`pano.commit_all(...)` (Panorama). Pass `exception=True` if you want
failed commits to raise `PanCommitFailed` rather than return a dict
with `success=False`.

**Q: What is `vsys=` for, and when do I set it?**

On a multi-vsys firewall, vsys-rooted objects (`Zone`, `Interface`,
`AddressObject` in some configs, `Rulebase`, etc.) live under a
specific vsys. Set `Firewall(..., vsys="vsys2")` to scope. The default
is `vsys1`. Panorama's analog is `DeviceGroup` (for policy/objects)
and `Template` (for network/device).

**Q: How do I bulk-create or bulk-update many objects?**

Build them all in your tree first, then call `create_similar()`,
`apply_similar()`, or `delete_similar()` on **any one** of them. The
SDK collects every sibling of the same class and sends a single API
call. See **Recipe 3** and the bundled `examples/bulk_address_objects.py`.

**Q: Can I use username/password and an API key in the same project?**

Yes. Use username/password the first time to fetch the key (read
`fw.api_key`). Persist it. From then on, instantiate with `api_key=`
only. The SDK never re-authenticates if the key is set.

**Q: How do I tell if a refresh hit the running vs. the candidate config?**

Pass `running_config=True` to `refresh()` or `refreshall()`. Default is
candidate (which matches the GUI's "view candidate" pane).

**Q: Does pan-os-python support PAN-OS 11.x?**

Yes. The SDK uses per-class `VersionedParamPath` profiles that switch
the schema based on the device's reported version.
`fw.refresh_system_info()` populates `fw.version`, and subsequent
calls pick the right profile automatically.

**Q: How do I work with Prisma Access?**

The `panos.plugins` module contains `CloudServicesPlugin`,
`RemoteNetwork`, `RemoteNetworks`, `AggBandwidth`, `Bgp`, `Tenants`,
etc. Five bundled examples (`prisma_access_*.py`) show the patterns:
attach `CloudServicesPlugin` to a Panorama, refresh, then walk to
remote networks, regions, and bandwidth allocations.

**Q: How do I know which child types a parent supports?**

Read the class's `CHILDTYPES` tuple. For example,
`Firewall.CHILDTYPES` lists `objects.AddressObject`, `policies.Rulebase`,
`network.EthernetInterface`, and ~50 more. `DeviceGroup.CHILDTYPES`
is a shorter list focused on objects, policies, and child Firewalls.

**Q: How do I tag a user with User-ID?**

`fw.userid.tag_user(user, tags, timeout=None, prefix=None)` -- for a
known user. For an IP-based mapping, use
`fw.userid.register(ip, tags, timeout)` (Dynamic Address Group). See
**Recipe 11**.

**Q: Where are the examples?**

In the `examples/` directory of the source repo:
`bulk_address_objects.py`, `bulk_subinterfaces.py`,
`dyn_address_group.py`, `ensure_security_rule.py`,
`log_forwarding_profile.py`, `upgrade.py`, `userid.py`, plus five
`prisma_access_*.py` scripts. The `tests/live/` directory is also a
goldmine -- those tests exercise nearly every class in the SDK
against a real device.

**Q: Is there an async / asyncio API?**

No. The SDK is synchronous. Use threading or multiprocessing if you
need to fan out across many devices. Each `Firewall`/`Panorama`
instance is not safe to share across threads -- instantiate one per
worker.

**Q: How do I see what API call the SDK actually sent?**

Set logging to `panos.DEBUG2` (or deeper). The XML payload, URL, and
response are emitted to the standard `logging` module.

---

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