pan-os-python User Guide

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.

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

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:

from panos.firewall import Firewall

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

Run it:

python hello_panos.py

Expected output (truncated):

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)

pip install pan-os-python

Pinned version

pip install "pan-os-python==1.12.1"

Inside a virtualenv

python -m venv .venv
source .venv/bin/activate           # macOS/Linux
# .venv\Scripts\activate            # Windows PowerShell
pip install pan-os-python

Poetry

poetry add pan-os-python

Editable / dev install

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

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

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

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

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.

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:

import os
from panos.firewall import Firewall

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

Then:

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:

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:

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

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

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.

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.

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:

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.

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:

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.

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.

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.

# 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:

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.

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.

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.

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

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:

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.

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:

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:

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.