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_keyis 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
Firewall("192.0.2.10", "admin", "...")constructs the device root of an object tree. No network I/O yet.fw.refresh_system_info()issued akeygencall (because noapi_key=was passed), then ranshow system info, and parsed the XML into theSystemInfonamed tuple.- The SDK populated
fw.version,fw.platform,fw.serial, andfw._version_infoso 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 inpan-python==0.17.0as a hard pin; mixing it with conflicting versions ofpan-pythonalready 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-pythonas root. Use a virtualenv or the--userflag. 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:
- Constructor arguments -- the
hostname,api_username,api_password,api_key,port,vsys,serial,is_virtual,timeout, andintervalyou pass toFirewall(...)orPanorama(...). - 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. - Python
logging-- the SDK uses the standardloggingmodule 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.
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:
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. CaptureDEBUG3+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 -> Rulebaseand walks the tree from there; it never instantiates aVsys,Zone, orInterface.
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 APItype=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, callingapply()on the parent silently truncates the others. When in doubt, usecreate().
Anatomy of a typical script
Almost every workflow follows the same five steps:
- Construct the device root --
Firewall(...)orPanorama(...). No I/O. - Attach the children you need --
fw.add(Rulebase()),pano.add(DeviceGroup(name="DG1")). Still no I/O. - Pull current state --
SecurityRule.refreshall(rulebase)orobj.refresh(). - Mutate Python objects in memory.
- Push back --
.create()for new objects,.apply()to replace,.delete()to remove. Thenfw.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 bundledexamples/bulk_address_objects.pydemonstrates a 600-object create that completes in roughly the time of a singlecreate().
Authentication: API key vs. username/password
Three patterns are supported:
- Username + password -- pass
api_usernameandapi_passwordto the constructor. The SDK callskeygenon the first device-touching method and caches the key on the object. - Pre-issued API key -- pass
api_key=to the constructor. The SDK skipskeygenentirely. Get the key once with the username/password form, then readfw.api_keyand persist it. - Auto-detect device type --
PanDevice.create_from_device(host, user, pw)connects, runsshow system info, then returns either aFirewallorPanoramainstance 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
keygenonce 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 withcmd_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 withAddressObject.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)andrefreshall(..., 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 aPanoramainstance is unsupported. The bundledexamples/dyn_address_group.pyshows the canonical pattern withPanDevice.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 withdryrun=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.
- Preferred. Install the firewall's CA cert into your trust store
(
certifi.where()location, OS keychain, orREQUESTS_CA_BUNDLE). - 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.xapiSSL 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.
- From the host running your script:
nc -zv <fw-ip> 443orcurl -kvI https://<fw-ip>/. - Confirm the management profile permits the source IP (Device -> Setup -> Management -> Permitted IP Addresses).
- 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.