This guide is for engineers who integrate, deploy, and contribute to
pan-os-python itself, or who run automation built on it at scale --
operations engineers, platform/SRE teams, security architects, and SDK
contributors. Architecture, the versioned object model, the wrapping
layer over pan.xapi.PanXapi, the exception hierarchy, deployment
patterns, operational concerns, security analysis, and how to extend the
SDK.
For task-oriented usage (connecting, adding rules, bulk operations), see the User Guide.
1. Architecture Overview
pan-os-python is a thin, opinionated wrapper around the PAN-OS XML API.
It does five things:
- Builds a typed Python object tree that mirrors the firewall's XML
configuration (
PanDevice->Vsys/DeviceGroup->AddressObject,SecurityRule,Interface, etc.). - Translates the tree to XML and XPaths automatically. You set
rule.action = "deny"; the SDK figures out the right XPath, the right XML envelope, and the right API call (type=setvstype=editvstype=delete). - Tracks PAN-OS schema evolution through per-class
VersionedParamPathprofiles, so the same Python class supports PAN-OS 6.1 through 11.x by emitting different XPaths/elements based on the device's version. - Wraps
pan.xapi.PanXapi-- the lower-level HTTP/XML transport library -- viaXapiWrapper, classifying transport exceptions into the SDK's own exception hierarchy. - Exposes operational surfaces --
userid,predefined,software,content,op(),commit(), hit-count refresh, audit-comment logs, HA helpers, and the Panorama device-group hierarchy.
Request flow (top to bottom)
+-----------------------------------------------------------------+
| Your script |
| fw.add(rb).add(SecurityRule(name="x", action="deny")) |
| rule.create() |
+-----------------------------------------------------------------+
|
v
+-----------------------------------------------------------------+
| panos.base.VersionedPanObject |
| .create() -> .element() -> XML for this node + children |
| resolves XPath via parent chain & device version |
+-----------------------------------------------------------------+
|
v
+-----------------------------------------------------------------+
| panos.base.PanDevice.xapi |
| -> XapiWrapper.set/edit/delete/get/keygen/op |
| -> classify_exception() maps PanXapiError -> Pan*Error |
+-----------------------------------------------------------------+
|
v
+-----------------------------------------------------------------+
| pan.xapi.PanXapi (third-party 'pan-python==0.17.0') |
| HTTP/HTTPS to firewall management plane TCP/443 |
| handles auth, retries, response parsing |
+-----------------------------------------------------------------+
|
v
+-----------------------------------------------------------------+
| PAN-OS XML API on the firewall / Panorama mgmt interface |
+-----------------------------------------------------------------+
Class-tree backbone (confirmed in panos/base.py)
PanObject
+-- PanDevice (in panos/base.py)
| +-- Firewall (panos/firewall.py)
| +-- Panorama (panos/panorama.py)
+-- VersionedPanObject (everything that maps to PAN-OS XML)
+-- VsysOperations (objects that get imported into a vsys)
| +-- Vlan, Interface, VirtualWire, VirtualRouter,
| LogicalRouter, Vrf, ...
+-- ValueEntry (single-value entries, e.g. HTTP headers)
+-- AddressObject, AddressGroup, ServiceObject, ServiceGroup,
Tag, ApplicationObject, ApplicationGroup, ScheduleObject,
Region, Edl, LogForwardingProfile, ...
+-- SecurityRule, NatRule, DecryptionRule, AuthenticationRule,
ApplicationOverride, PolicyBasedForwarding
+-- Rulebase, PreRulebase, PostRulebase
+-- Zone, EthernetInterface, AggregateInterface,
Layer2Subinterface, Layer3Subinterface, ManagementProfile,
IkeGateway, IpsecTunnel, IpsecCryptoProfile,
IkeCryptoProfile, GreTunnel, Dhcp, ...
+-- DeviceGroup, Template, TemplateStack, TemplateVariable
+-- HighAvailability, HA1, HA2, HA3
+-- CloudServicesPlugin, RemoteNetwork, RemoteNetworks,
AggBandwidth, Tenants (panos/plugins.py)
Helper / non-config classes:
VarPath, ParamPath, VersionedParamPath, VersioningSupport,
VersionedStubs, ParentAwareXpath (XPath/version dispatch in panos/base.py)
OpState, OpStateContainer (operational-state plumbing)
Predefined (panos/predefined.py)
SoftwareUpdater, ContentUpdater (panos/updater.py)
UserId (panos/userid.py)
XapiWrapper (panos/base.py)
Component inventory
| Component | Path | Role |
|---|---|---|
| Core base classes | panos/base.py |
PanObject, VersionedPanObject, PanDevice, XapiWrapper, XPath/version machinery |
| Firewall device | panos/firewall.py |
Firewall, FirewallState, FirewallCommit, vsys handling |
| Panorama device | panos/panorama.py |
Panorama, DeviceGroup, Template, TemplateStack, TemplateVariable, commit-all |
| Policies | panos/policies.py |
Rulebase, PreRulebase, PostRulebase, SecurityRule, NatRule, DecryptionRule, AuthenticationRule, ApplicationOverride, PolicyBasedForwarding, hit count, audit comments |
| Objects | panos/objects.py |
AddressObject, AddressGroup, ServiceObject, ServiceGroup, Tag, ApplicationObject/Group/Filter/Container/Tag, SecurityProfileGroup, CustomUrlCategory, LogForwardingProfile, DynamicUserGroup, ScheduleObject, Region, Edl |
| Network | panos/network.py |
Zone, Vlan, EthernetInterface, AggregateInterface, subinterfaces, VirtualRouter, LogicalRouter, Vrf, OSPF/BGP/RIP, IPsec/IKE, GRE, DHCP, advanced routing |
| Device | panos/device.py |
Vsys, SystemSettings, NTP, admins, syslog/SNMP/email/HTTP/LDAP server profiles, certificate profiles, SSL decrypt |
| HA | panos/ha.py |
HighAvailability, HA1/HA2/HA3, peer management |
| Errors | panos/errors.py |
22 exception classes rooted at PanDeviceError |
| Updater | panos/updater.py |
SoftwareUpdater, ContentUpdater |
| User-ID | panos/userid.py |
UserId subsystem (login/logout/register/tag) |
| Predefined | panos/predefined.py |
Predefined applications/services/tags |
| Plugins | panos/plugins.py |
Prisma Access / Cloud Services plugin objects |
Technology stack
| Layer | Technology | Notes |
|---|---|---|
| Language | Python 3.6+ | Per panos/__init__.py (RuntimeWarning on older versions) |
| Transport | pan-python==0.17.0 |
Hard pin via requirements.txt |
| XML | xml.etree.ElementTree (stdlib) |
Used throughout panos/base.py, panos/policies.py, etc. |
| Versioning | distutils.version.LooseVersion |
Subclassed to PanOSVersion (Python 3.12+ caveat per issues #580, #529) |
| Logging | stdlib logging |
With four extra debug levels (DEBUG1-DEBUG4) |
| Build | pyproject.toml (Poetry) |
Release automation via .releaserc.json (semantic-release) |
| Docs | Sphinx + m2r + sphinx_rtd_theme |
Read the Docs build at PaloAltoNetworks/pan-os-python |
Note
The SDK does NOT use
requests,httpx,aiohttp, or any async I/O. All transport runs throughpan.xapi.PanXapi, which uses Python's stdliburllib. Concurrency is the caller's responsibility -- use threading or multiprocessing.
2. System Requirements
Runtime
| Requirement | Value |
|---|---|
| Python | 3.6+ (older versions emit RuntimeWarning) |
| OS | Any platform CPython runs on |
| Memory | Trivial -- typical scripts use 50-200 MB; large refreshall against complex Panoramas can spike to 1-2 GB |
| Network | Outbound TCP/443 to firewall/Panorama management interface |
| Permissions | Admin account on device, or pre-issued API key |
Build / development
| Requirement | Value |
|---|---|
| Poetry | Recommended (per pyproject.toml + poetry.lock) |
pip install -e . |
Supported alternative |
mypy.ini |
Static type checking config present |
.flake8, .isort.cfg |
Linting/formatting profiles |
tox.ini |
Multi-Python test matrix |
.bandit |
Security linter config |
| Sphinx | For doc builds (see docs/conf.py, docs/Makefile) |
Hard dependency
pan-python==0.17.0 (cryptographically pinned via SHA-256 hashes in requirements.txt)
This is the only runtime dependency. There are no optional extras.
Important
The pin on
pan-python==0.17.0is exact. Mixing in a different version (e.g., a system package or another package's transitive requirement) results in subtle XML-handling drift. Always install the SDK in an isolated virtualenv.
3. Configuration Reference
The SDK has no config files. Every knob is a constructor argument or a module-level Python attribute. This section enumerates them.
panos.base.PanDevice (abstract base)
PanDevice(
hostname, # required: hostname or IP
api_username=None, # admin username
api_password=None, # admin password
api_key=None, # pre-issued API key
port=443, # mgmt-plane TCP port
is_virtual=None, # True for VM-Series; auto-detected
timeout=1200, # async-job seconds
interval=0.5, # async-job poll interval
*args, **kwargs,
)
| Param | Type | Default | Description |
|---|---|---|---|
hostname |
str |
(required) | Hostname or IP of the device |
api_username |
str |
None |
Admin username; required if no api_key |
api_password |
str |
None |
Admin password; paired with api_username |
api_key |
str |
None |
Pre-issued API key; bypasses keygen |
port |
int |
443 |
Management TCP port |
is_virtual |
bool |
None |
Hint for VM-Series detection |
timeout |
int |
1200 |
Seconds for synchronous long jobs (commit, syncreboot) |
interval |
float |
0.5 |
Polling interval for async jobs |
panos.firewall.Firewall
Firewall(
hostname=None,
api_username=None,
api_password=None,
api_key=None,
serial=None, # serial of the firewall
port=443,
vsys=None, # 'vsys1', ..., 'shared', or None
is_virtual=None,
multi_vsys=None,
*args, **kwargs,
)
Additional kwargs accepted:
| Kwarg | Description |
|---|---|
vsys_name |
Display name of the vsys |
serial_ha_peer |
Serial of the HA peer |
management_ip |
Management IP override |
Class-level attributes:
| Attribute | Default | Purpose |
|---|---|---|
XPATH |
"/devices" |
Root XPath for the firewall |
ROOT |
Root.MGTCONFIG |
Where this object lives in the config tree |
SUFFIX |
ENTRY |
XML element naming convention |
NAME |
"serial" |
Field used as primary identifier |
DEFAULT_VSYS |
"vsys1" |
vsys used when none is specified |
CHILDTYPES: ~50 entries spanning device.*, objects.*,
policies.*, network.*, and ha.HighAvailability. See
panos/firewall.py for the canonical list.
panos.panorama.Panorama
Panorama(
hostname,
api_username=None,
api_password=None,
api_key=None,
port=443,
*args, **kwargs,
)
Class-level attributes:
| Attribute | Default | Purpose |
|---|---|---|
FIREWALL_CLASS |
firewall.Firewall |
Class used when instantiating managed firewalls |
NAME |
"hostname" |
Primary identifier |
DEFAULT_VSYS |
"shared" |
vsys placeholder for Panorama-shared scope |
CHILDTYPES: includes firewall.Firewall, panorama.DeviceGroup,
panorama.Template, panorama.TemplateStack, plus shared objects.*
and a subset of device.* server-profile classes.
panos.panorama.DeviceGroup
DeviceGroup(
name, # device-group name
tag=None, # list of tags
)
| Param | Type | Description |
|---|---|---|
name |
str |
Device-group name (required, primary key) |
tag |
list[str] |
Administrative tags |
panos.panorama.Template
Template(
name,
description=None,
devices=None, # list of serial numbers
default_vsys=None, # 7.0+ ; legacy
multi_vsys=None, # 6.1 and lower
mode=None, # 6.1 and lower
vpn_disable_mode=None, # 6.1 and lower
)
panos.panorama.TemplateStack
TemplateStack(
name,
description=None,
templates=None, # list of template names in stack order
devices=None, # list of serial numbers
)
panos.policies.SecurityRule
The most-used rule class. Constructor accepts (per panos/policies.py):
SecurityRule(
name,
fromzone=["any"], tozone=["any"],
source=["any"], source_user=["any"],
hip_profiles=["any"], # PAN-OS 10.0.0- only
destination=["any"], application=["any"],
service="application-default", # default value, can be a list
category=["any"],
action=None, # 'deny','allow','drop','reset-client','reset-server','reset-both'
log_setting=None,
log_start=None, log_end=None,
description=None,
type="universal", # 'universal', 'intrazone', 'interzone'
tag=None,
negate_source=None, negate_destination=None,
disabled=None,
schedule=None,
icmp_unreachable=None,
disable_server_response_inspection=None,
group=None, # security profile group
negate_target=None, target=None, # Panorama device-group only
virus=None, spyware=None, vulnerability=None,
url_filtering=None, file_blocking=None,
wildfire_analysis=None, data_filtering=None,
uuid=None, # PAN-OS 9.0+
source_devices=["any"], # PAN-OS 10.0+
destination_devices=["any"], # PAN-OS 10.0+
group_tag=None, # PAN-OS 9.0+
)
Operational state subobjects:
rule.opstate.audit_comment--RuleAuditComment(PAN-OS 9.0+)rule.opstate.hit_count--HitCount(PAN-OS 8.1+)
HIT_COUNT_STYLE = "security" is set on the class, used by
RulebaseHitCount.refresh(style="security", ...).
panos.policies.NatRule
NatRule has 30+ source-translation/destination-translation/dynamic
parameters. Highlights (per panos/policies.py):
NatRule(
name, description=None,
nat_type="ipv4", # 'ipv4', 'nat64', 'nptv6'
fromzone=["any"], tozone=None,
to_interface=None,
service="any",
source=["any"], destination=["any"],
source_translation_type=None, # 'dynamic-ip-and-port', 'dynamic-ip', 'static-ip'
source_translation_address_type=None,
source_translation_interface=None,
source_translation_ip_address=None,
source_translation_translated_addresses=None,
source_translation_fallback_type=None,
... # ~14 more source-translation params
destination_translated_address=None,
destination_translated_port=None,
ha_binding=None, # 'primary','both','0','1'
disabled=None,
negate_target=None, target=None,
tag=None,
destination_dynamic_translated_address=None, # PAN-OS 8.1+
destination_dynamic_translated_port=None, # PAN-OS 8.1+
destination_dynamic_translated_distribution=None,
uuid=None, # PAN-OS 9.0+
group_tag=None, # PAN-OS 9.0+
)
panos.objects.AddressObject
AddressObject(
name,
value, # IP, range, wildcard, or fqdn
type="ip-netmask", # 'ip-netmask','ip-range','ip-wildcard','fqdn'
description=None,
tag=None, # list of tag names
)
Note
ip-wildcardwas added in PAN-OS 9.0. Older devices reject it -- the SDK will pass the value through; the firewall returns the validation error.
panos.objects.AddressGroup
AddressGroup(
name,
static_value=None, # list of address names (static group)
dynamic_value=None, # tag-match expression (dynamic group)
description=None,
tag=None,
)
A given group is either static (uses static_value) or dynamic (uses
dynamic_value) -- not both.
panos.objects.Tag
Tag(
name,
color=None, # 'color1' through 'color42' (gaps); use Tag.color_code()
comments=None,
)
Tag.color_code(color_name) resolves a human color ("red",
"forest green", "midnight blue", etc.) into the colorN ID PAN-OS
expects.
panos.objects.ServiceObject
ServiceObject(
name,
protocol="tcp", # 'tcp', 'udp', or (8.1+) 'sctp'
source_port=None,
destination_port=None,
description=None,
tag=None,
enable_override_timeout=None, # PAN-OS 8.1+
override_timeout=None,
override_half_close_timeout=None,
override_time_wait_timeout=None,
)
panos.objects.ApplicationObject
Has 30+ attributes describing application signatures -- category,
subcategory, technology, risk, port-list, evasion behaviors, file
types. See panos/objects.py (line ~320).
Logging
Module-level constants in panos/__init__.py:
panos.DEBUG1 # logging.DEBUG - 1 (method tracing)
panos.DEBUG2 # DEBUG1 - 1 (XML payloads)
panos.DEBUG3 # DEBUG2 - 1 (deeper XML)
panos.DEBUG4 # DEBUG3 - 1 (everything)
panos.getlogger(name) returns a configured logger with
debug1/debug2/debug3/debug4 convenience methods.
Important
Production code should run at
WARNINGorINFO. The deeper levels emit credentials and certificate material in cleartext. See Section 7.
4. Deployment & Scaling
Deployment patterns
| Pattern | Where the script runs | Trade-offs |
|---|---|---|
| Direct from a workstation | Engineer's laptop or jump host | Simple; tied to a human session |
| Containerized cron | A long-running container with cron and the SDK | Idempotent, observable via container logs |
| CI/CD job | GitHub Actions, GitLab CI, etc. | Auditable history of every config change |
| Workflow engine | Airflow, Argo, Temporal, AWX/Tower | Retries, scheduling, distributed execution |
| Serverless | AWS Lambda, Cloud Run | Cold-start cost; size limits OK (small image) |
| Bastion / mgmt-only host | A locked-down jump host with mgmt access | Most secure; standard for production |
The SDK has no daemon, no agent, no outbound listening port. It runs on demand, makes API calls, exits.
Scaling concurrency
The SDK is synchronous -- one HTTP request at a time per
PanDevice instance. Two scaling axes:
- Many devices in parallel. Instantiate one
Firewall/Panoramaper worker thread or process. Each instance has its ownXapiWrapperand its own API key. - Many objects per device. Use
create_similar(),apply_similar(),delete_similar()instead of loopingcreate()/apply()/delete(). One call instead of N. The bundledbulk_address_objects.pyexample creates 600 objects in one round trip.
Tip
For Panorama with 100+ managed firewalls, use a
concurrent.futuresthread pool with oneFirewallinstance per worker. The Panorama push API (commit_all) is itself parallel server-side, so the client-side win is mostly about parallelizing the fetches.
Sizing the pool
| Device count | Recommended workers | Notes |
|---|---|---|
| 1-10 | 4 | Single-threaded is fine; 4 keeps total wall time reasonable |
| 10-50 | 8-16 | Watch firewall mgmt-plane CPU |
| 50-200 | 16-32 | Beyond this, you're stressing Panorama; consider using Panorama.refresh_devices() once and caching |
| 200+ | Custom | Talk to Palo Alto support before going wide; mgmt plane is not built for this |
Connection-pool settings
PanXapi reuses a single HTTPS connection per instance. There's no
explicit pool tuning. Long-running scripts should re-instantiate the
device object every few hours to avoid stale TLS sessions.
Job/timeout tuning
| Setting | Default | Tune up if... |
|---|---|---|
Firewall(timeout=1200) |
20 min | Commits on large Panoramas can exceed the default in extreme cases |
Firewall(interval=0.5) |
0.5 s | Lower for small firewalls (faster polling); raise for very large pushes |
commit(sync=True) |
blocks | Use sync_all=True on Panorama to also wait for downstream device jobs |
Note
A "commit" on Panorama is local -- it commits the candidate to the Panorama running config. To push to managed firewalls, call
Panorama.commit_all(...). Setsync_all=Trueif you want to block until the per-device push jobs complete.
5. Operations
Day-2 runbooks
Health check
A minimal "is it alive" probe:
from panos.firewall import Firewall
fw = Firewall(host, api_key=key)
print(fw.refresh_system_info()) # version, model, serial
print(fw.show_system_resources()) # CPU/memory
print(fw.is_ready()) # True if mgmt plane is up
is_ready(minutes=10, seconds=0) blocks until the device responds (up
to the timeout) -- useful right after a reboot.
Backup a firewall config
There's no first-class backup() method, but the pattern is one
op-command:
xml = fw.op("show config running")
with open(f"{fw.serial}-{date.today()}.xml", "wb") as f:
f.write(ET.tostring(xml.find(".//config"), encoding="utf-8"))
Caution
The dumped XML contains every secret on the box --
phashadmin password hashes, certificate private keys, IPsec PSKs, RADIUS shared secrets. Treat backup files as Tier-1 secrets. Encrypt at rest. See Section 7.
Lock-take, modify, lock-release
fw.add_config_lock(comment="rule push by automation", exceptions=True)
try:
# ... mutate the tree ...
fw.commit(sync=True, exception=True)
finally:
fw.remove_config_lock()
Locks block other admins from concurrent commits. add_commit_lock()
is the sibling for commit-time locking.
HA awareness
fw.refresh_ha_active() # query active/passive state
fw.is_active() # bool
fw.toggle_ha_active() # force failover (read code first!)
fw.synchronize_config() # force HA config sync
fw.config_sync_state() # check sync status
fw.config_synced() # bool
Firewall.set_ha_peers(device) wires together two Firewall
instances so you can call map_ha(method_name) -- the method runs
against the active peer automatically.
Monitoring
| Signal | Source | What it tells you |
|---|---|---|
fw.refresh_system_info() |
<show><system><info> |
Liveness, version |
fw.show_system_resources() |
<show><system><resources> |
CPU/memory |
pano.refresh_devices() |
Panorama | Per-managed-device connectivity |
pano.commit_all(...) exit |
commit-all job result |
Push success/failure |
RulebaseHitCount.refresh() |
Per-rulebase op | Stale-rule detection |
fw.config_sync_state() |
<show><high-availability><state> |
HA drift |
Tip
Pipe
refresh_system_info()into a TSDB once a minute. It's cheap and gives you a second heartbeat independent of SNMP/syslog.
Periodic maintenance
| Cadence | Task | Why |
|---|---|---|
| Hourly | Health check across fleet | Early warning |
| Daily | Backup running config | Recovery |
| Weekly | Hit-count refresh, identify zero-hit rules | Policy hygiene |
| Monthly | API key rotation | Credential hygiene |
| Quarterly | Audit admin accounts via device.Administrator.refreshall() |
Access review |
Common operational pitfalls
- Forgetting to commit.
create()writes to candidate, not running. Schedule a syncfw.commit(sync=True)after batches. - Calling
apply()on a stale tree. If you only refreshed one child but callapply()on the parent, you can wipe siblings. Refresh the full sibling set first. - Blocking on async pushes.
commit_allwithoutsync_all=Truereturns once Panorama accepts the job; the per-device jobs run asynchronously. Always poll orsync_all=Trueif your script must know they're done. - API rate / connection limits. Mgmt-plane CPU is the bottleneck; exceeding ~30 concurrent API calls per firewall causes the device to queue or reject.
6. Performance
Where the time goes
For a typical "fetch all rules, find the matching one, modify, push" script:
| Phase | Wall-clock |
|---|---|
keygen (first call) |
100-300 ms |
refreshall(SecurityRule) (typical rulebase) |
200 ms - 5 s |
Per-rule .apply() / .create() |
200-500 ms each |
commit(sync=True) |
5 s - 10+ min |
commit_all(sync_all=True) (Panorama) |
30 s - 60+ min |
commit_all dominates Panorama workflows. Optimize by batching --
make all changes, then commit once.
Bulk operations are non-negotiable for scale
create() over 600 address objects: ~2 minutes (one HTTP round trip
per object).
create_similar() over the same 600: ~3 seconds (one HTTP round
trip total).
The bundled examples/bulk_address_objects.py demonstrates and times
this. Use bulk for every scenario where you're touching > ~10 objects
of the same class.
refreshall strategies
# Default: pull every entry with all params -> verbose XML
SecurityRule.refreshall(rb)
# Just names, no params -> tiny XML, fast
SecurityRule.refreshall(rb, name_only=True)
# Don't add to the tree -> useful for read-only audits
SecurityRule.refreshall(rb, add=False)
# Hit running config instead of candidate
SecurityRule.refreshall(rb, running_config=True)
name_only=True is a 5-50x speedup for "discovery" workloads -- list
what's there, then individually refresh() only what you'll modify.
Avoid re-instantiation churn
- One
Firewall(...)instance per device per script. Don't re-instantiate on every iteration of a loop --keygenis paid every time you do. predefined,software,content,useridare created lazily as attributes ofPanDevice.__init__. They're cheap, but reusing the parent device object is still cheaper.
Panorama scaling
Panorama.refresh_devices(devices=None, only_connected=True, expand_vsys=True, include_device_groups=True, add=True, running_config=False) is one big call that gets the whole managed
device list, their vsyses, and their device-group attachments. Use it
once at the top of a Panorama-wide script and walk the resulting tree
in memory.
Note
expand_vsys=Truematerializes oneFirewallinstance per vsys per managed device. On a Panorama managing 100 multi-vsys firewalls, you get hundreds of objects. Passexpand_vsys=Falseif you only need per-chassis data.
7. Security
Credential handling
The SDK keeps API credentials in process memory only. No file is
written, no environment variable is exported on its own. After
keygen, fw.api_key is set on the PanDevice instance and stays
there for the life of the process.
Three rules:
- Pull credentials from a secret manager (Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, 1Password CLI). Inject as environment variables for the duration of the script.
- Use API keys, not passwords, in production scripts. Username/password is fine for a one-off interactive run.
- Rotate API keys regularly. Every API key is a long-lived credential -- if compromised, an attacker has full mgmt-plane access under that admin's role until the key is revoked.
Warning
Never commit
.envfiles containing PAN-OS credentials. Add.env,*.api_key, andsecrets.*to your repo's.gitignore. Audit your CI logs, and keep API keys out of exception messages and stack traces.
TLS verification
By default pan.xapi.PanXapi performs full TLS verification against
the system trust store. The SDK does not expose a "skip verification"
toggle.
- Production. Install your enterprise CA into the OS trust store,
or set
REQUESTS_CA_BUNDLE/SSL_CERT_FILEto point at a bundle containing the firewall's CA. - Lab. If a self-signed cert is genuinely unavoidable for a one-off
test, the documented workaround is to monkey-patch the SSL context
on
pan.xapi.PanXapi. Do this in the script itself, never as a global setting.
Caution
Disabling TLS verification removes any defense against MITM attack on the management plane. An attacker with access to the management network could harvest credentials on the first connection. Don't normalize "skip verification" as your team's default.
Protecting exported configurations
A configuration exported from a firewall or Panorama (for example with op("show config running"), or by walking
the SDK tree and serializing it) is sensitive material: it can include administrator password hashes, certificates
and keys, and the credentials of the services the device connects to (LDAP, RADIUS, syslog and similar).
- Store exports like other credential material: encrypted at rest, access limited to the people who need them, deleted when change management no longer needs them.
- If an export is shared outside your team (for example with a vendor for support), remove the sensitive values first and follow your data-handling policy.
- Rotate the API keys your scripts use on a known cadence (see Credential handling above).
For how PAN-OS protects stored secrets (including the master key), use the official PAN-OS administrator documentation as the reference, not this guide.
What NOT to log
- API keys -- redact
fw.api_keybefore logging objects. - Passwords -- if you use username/password mode, never log the
Firewallinstance or its__dict__;_api_passwordis stored as a private attribute. DEBUG3/DEBUG4traces -- these contain full request bodies, including candidate-config snippets with credentials. Capture only to a local file you control; never to a SIEM/log aggregator.- Stack traces from
PanCommitFailed-- the.resultattribute can contain the snippet that failed validation, which may include a pre-shared key you were trying to set.
Warning
Configure your global Python logging filter or
logging.Filtersubclass to scrubapi_key=,api_password=, andphash=patterns from any log record before it leaves the host.
Authorization model
The SDK has no opinion about admin roles -- it uses whatever role is attached to the API key/admin you authenticated as. To limit blast radius, create a dedicated admin role on the firewall (XML API permission scope, no GUI) and use that for automation. Avoid using superuser admin accounts in scripts.
device.Administrator is the SDK class that manages admin accounts
themselves; review with device.Administrator.refreshall(fw).
Network exposure
pan-os-python does not listen on any port. There's no inbound surface.
The only egress is HTTPS to the firewall management plane.
8. Developer Internals
Repository layout
pan-os-python/
+-- panos/
| +-- __init__.py Module-level helpers, PanOSVersion, getlogger
| +-- base.py PanObject, VersionedPanObject, PanDevice, XapiWrapper
| +-- errors.py 22 exception classes
| +-- firewall.py Firewall, FirewallState, FirewallCommit
| +-- panorama.py Panorama, DeviceGroup, Template, TemplateStack, ...
| +-- policies.py Rulebase, SecurityRule, NatRule, hit count, audit comments
| +-- objects.py Address/Service/Application objects, Tag, EDL, ...
| +-- network.py Zones, interfaces, routers, OSPF/BGP/RIP, IPsec/IKE
| +-- device.py Vsys, SystemSettings, NTP, server profiles, certs, admins
| +-- ha.py HighAvailability, HA1/2/3
| +-- predefined.py Predefined applications/services/tags
| +-- updater.py SoftwareUpdater, ContentUpdater
| +-- userid.py User-ID subsystem
| +-- plugins.py CloudServicesPlugin, RemoteNetwork, Prisma Access
+-- examples/
| +-- bulk_address_objects.py
| +-- bulk_subinterfaces.py
| +-- dyn_address_group.py
| +-- ensure_security_rule.py
| +-- log_forwarding_profile.py
| +-- prisma_access_create_remote_network.py
| +-- prisma_access_list_RN_regions_bw.py
| +-- prisma_access_show_jobs_status.py
| +-- prisma_access_show_remote_net_per_tenant.py
| +-- upgrade.py
| +-- userid.py
+-- tests/
| +-- test_*.py Unit tests (pytest)
| +-- live/
| +-- conftest.py Fixtures: fw, dev, pano, pairing
| +-- testlib.py FwFlow, DevFlow, PanoFlow base test classes
| +-- test_device_config.py
| +-- test_network.py
| +-- test_objects.py
| +-- test_set_reference.py
| +-- test_userid.py
+-- docs/ Sphinx config + module-*.rst pages
+-- pyproject.toml
+-- requirements.txt Pins pan-python==0.17.0 with SHA-256 hashes
+-- tox.ini
+-- .flake8 / .isort.cfg / mypy.ini / .bandit
+-- .releaserc.json Semantic-release config
The metaclass / object-tree machinery
Three machinery classes in panos/base.py run the show:
PanObject-- root class. Implements the in-memory tree (add,insert,extend,pop,remove,find,findall), XPath construction (xpath,xpath_short,xpath_root), and the basic CRUD verbs (apply,create,delete,refresh).VersionedPanObject(PanObject)-- adds version awareness via_xpaths(aParentAwareXpath) and_params(a tuple ofVersionedParamPath). The__getattr__/__setattr__overrides proxy attribute access through the version profile sorule.action = "deny"writes through to the right XML element for the device's reported PAN-OS version.VersioningSupport-- backing data structure for_xpaths/_params. Each entry is registered for a minimum PAN-OS version; lookup picks the highest version <= device version.
The classic shape of a VersionedPanObject subclass:
class AddressObject(VersionedPanObject):
ROOT = Root.VSYS
SUFFIX = ENTRY
def _setup(self):
self._xpaths.add_profile(value="/address")
params = []
params.append(VersionedParamPath("value", path="{type}"))
params.append(VersionedParamPath(
"type", default="ip-netmask",
values=["ip-netmask", "ip-range", "ip-wildcard", "fqdn"],
path="{type}",
))
params.append(VersionedParamPath("description", path="description"))
params.append(VersionedParamPath("tag", path="tag", vartype="member"))
self._params = tuple(params)
To add a parameter that exists only on PAN-OS 11.0+:
params.append(VersionedParamPath("new_field", exclude=True))
params[-1].add_profile("11.0.0", path="new-field")
The exclude=True profile is the "doesn't exist before" marker; it
prevents the SDK from sending the element on older devices.
Refresh strategies
refresh() (per-instance):
- Constructs the XPath for this object.
- Calls
xapi.get(xpath)(orxapi.show(xpath)ifrunning_config=True). - Walks the response, calls
parse_xml(elm)on this object, then recursively on all CHILDTYPES ifrefresh_children=True. - Replaces the in-memory params from the response.
refreshall(parent, ...) (class method):
- Computes the parent's XPath plus this class's relative XPath.
- Calls
xapi.getfor the whole list. - Iterates entries, calls
refreshall_from_xml(...)to build new instances of the class, attaches each toparent(ifadd=True).
Two performance levers: name_only=True (entry names only, no
params), and running_config=True (running config XPath; sometimes
faster on large candidates).
Exception hierarchy
Rooted at pan.xapi.PanXapiError (third-party):
PanXapiError (from pan.xapi)
PanDeviceError (from panos.errors)
PanDeviceXapiError
PanInvalidCredentials
PanURLError
PanConnectionTimeout
PanCommitInProgress
PanInstallInProgress
PanCommitFailed # has .result
PanCommitNotNeeded
PanSessionTimedOut
PanNoSuchNode
PanObjectMissing
PanHAConfigSyncFailed
PanHASyncInProgress
PanJobTimeout
PanLockError
PanPendingChanges
PanDeviceNotSet
PanNotConnectedOnPanorama
PanNotAttachedOnPanorama
PanObjectError
PanApiKeyNotSet
PanActivateFeatureAuthCodeError
PanOutdatedSslError
22 leaf classes total (per panos/errors.py and the class hierarchy
extraction). PanCommitFailed is the most useful one to catch
explicitly -- it carries .result with the validator messages.
Wrapping pan.xapi.PanXapi
XapiWrapper(pan.xapi.PanXapi) (in panos/base.py) is the integration
point:
make_method(super_method_name, super_method)-- generates wrapper methods at class-creation time that callsuper_methodand route any raisedPanXapiErrorthroughclassify_exception().classify_exception(e)-- pattern-matches the exception text/type and re-raises a more specificPan*Errorsubclass.
This is why every API call you make from your script can raise either
the generic PanXapiError or one of the SDK's specific subclasses.
Testing strategy
| Tier | Where | Runs against |
|---|---|---|
| Unit | tests/test_*.py |
Fixtures only; no network |
| Standards | tests/test_standards.py |
Lint-style class structure checks |
| Versioning | tests/test_versioning.py |
PanOSVersion ordering correctness |
| Live | tests/live/test_*.py |
A real firewall + Panorama (CI fixture) |
Live tests use the FwFlow/DevFlow/PanoFlow base classes
(tests/live/testlib.py), which provide a 5-step lifecycle:
setup_dependencies -> create -> refreshall -> update ->
cleanup. The pattern is worth studying if you write scripts that
need to be idempotent against live devices.
Adding a new class
The standard recipe to add a new firewall config type:
- Subclass
VersionedPanObjectin the appropriate module (policies.pyfor a rule,objects.pyfor an object,network.pyfor an interface, etc.). - Set
ROOT,SUFFIX,XPATH/_xpaths, and_paramsin_setup(). - Add it to the parent's
CHILDTYPES(e.g.,Firewall.CHILDTYPES,DeviceGroup.CHILDTYPES,Rulebase.CHILDTYPES). - Add a unit test in
tests/test_*.py. - Add a live test (subclass of
FwFlowintests/live/test_*.py) that exercises create/refresh/update/delete.
tests/test_classic_objects.py, tests/test_panorama.py, and
tests/test_userid.py are good reference reads.
Contributing
The repo follows conventional commits, semantic-release, Black-style
formatting, isort, mypy, flake8, and bandit. CI runs the unit test
matrix via tox. The live test suite requires a working firewall and
Panorama -- it's run manually, not on every PR.
9. Integration Points
What pan-os-python calls into
| Target | Purpose | How |
|---|---|---|
| Firewall mgmt plane | All config + op operations | HTTPS via pan.xapi.PanXapi |
| Panorama mgmt plane | Same, plus device-group / template / commit-all | HTTPS via pan.xapi.PanXapi |
That's it. No external services, no registries, no databases.
What might call into pan-os-python
| Caller | Pattern |
|---|---|
| Cron / systemd timer | Run a script on a schedule |
| CI/CD (GitHub Actions, GitLab CI) | Run on PR merge to push config |
| Workflow engines (Airflow, Argo, Temporal) | Multi-step orchestration |
| Web UI / Flask / FastAPI app | Wrap the SDK behind a REST endpoint for non-Python consumers |
| Ansible | The companion paloaltonetworks.panos Ansible collection wraps this SDK |
| Terraform | Not directly -- use the dedicated Terraform provider |
| Skyline-style monitors | Call op() repeatedly to scrape metrics |
Tip
If you're integrating with Ansible, prefer the official
paloaltonetworks.panoscollection, which uses this SDK under the hood and exposes idempotent module wrappers. Don't re-implement idempotency in your playbooks.
Plugin ecosystem (Prisma Access / Cloud Services)
panos.plugins provides typed objects for the Cloud Services /
Prisma Access plugin:
CloudServicesPlugin-- root attached to aPanorama.RemoteNetworks,RemoteNetwork-- remote network sites.AggBandwidth,Region,Link-- bandwidth allocation primitives.Tenants,AccessDomain-- multi-tenant support.InternalDnsMatch,PrimaryInternalDNSServer,SecondaryInternalDNSServer,PrimaryPublicDNSServer,SecondaryPublicDNSServer.Bgp,BgpPeer,RoutingPreference.
The five examples/prisma_access_*.py scripts demonstrate end-to-end
flows: create a remote network with IKE/IPsec, list regions and
bandwidth, show jobs, walk multi-tenant deployments.
User-ID integration
fw.userid.* is the Firewall-only User-ID API:
- User-IP mappings:
login,logins,logout,logouts. - IP-tag mappings (DAGs):
register,unregister,get_registered_ip,clear_registered_ip,audit_registered_ip,audit_registered_ip_for_tag. - User groups:
set_group,get_groups,get_group_members. - User tags (Dynamic User Groups):
tag_user,untag_user,get_user_tags. - Batching:
batch_start,batch_end,send-- accumulate multiple ops into one UID message.
Note
Use
batch_start()/batch_end()if you're scripting bulk SOAR responses (block 100 IPs at once). One UID message instead of 100 sequential calls.
Predefined-objects integration
device.predefined provides typed access to PAN-OS-shipped objects:
refresh_application(name),refresh_service(name),refresh_tag(name)-- pull a single predefined entry.refreshall_applications(),refreshall_services(),refreshall_tags(),refreshall()-- pull everything.application(name, refresh_if_none=True, include_containers=True),service(name),tag(name)-- accessor returning the cachedApplicationObject/ServiceObject/Tagafter refresh-if-needed.
This is useful for validation: before pushing a SecurityRule that
references application=["chrome-base"], verify the predefined app
exists on the target version.
Software / content updates
device.software (SoftwareUpdater) and device.content
(ContentUpdater) wrap the device's update subsystem:
- Software:
info(),check(),download(version),install(version),download_install(version),download_install_reboot(version),upgrade_to_version(target_version, dryrun). The latter handles intermediate hops automatically (e.g., 9.1 -> 10.1 -> 11.1). - Content:
info(),check(),download(sync_to_peer, sync),install(version, ...),download_and_install_latest(sync),downgrade(sync).
Caution
Content updates can change application identification and break existing rules unintentionally. Test in a lab vsys first, schedule production updates during a maintenance window.
CI/CD example
A minimal GitHub Actions step that pushes a security rule on PR merge:
- name: Push rule to firewall
env:
PAN_HOSTNAME: ${{ secrets.PAN_HOSTNAME }}
PAN_API_KEY: ${{ secrets.PAN_API_KEY }}
run: |
pip install pan-os-python==1.12.1
python scripts/ensure_rule.py
Pair with branch protection, required reviewers, and a "dry run"
script that calls refreshall and prints diffs without committing.