pan-os-python Administrator & Developer 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.

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

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


1. Architecture Overview

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

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

Request flow (top to bottom)

+-----------------------------------------------------------------+
|  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 through pan.xapi.PanXapi, which uses Python's stdlib urllib. Concurrency is the caller's responsibility -- use threading or multiprocessing.


2. System Requirements

Runtime

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

Build / development

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

Hard dependency

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

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

Important

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


3. Configuration Reference

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

panos.base.PanDevice (abstract base)

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-wildcard was added in PAN-OS 9.0. Older devices reject it -- the SDK will pass the value through; the firewall returns the validation error.

panos.objects.AddressGroup

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 WARNING or INFO. The deeper levels emit credentials and certificate material in cleartext. See Section 7.


4. Deployment & Scaling

Deployment patterns

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

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

Scaling concurrency

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

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

Tip

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

Sizing the pool

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

Connection-pool settings

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

Job/timeout tuning

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

Note

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


5. Operations

Day-2 runbooks

Health check

A minimal "is it alive" probe:

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 -- phash admin password hashes, certificate private keys, IPsec PSKs, RADIUS shared secrets. Treat backup files as Tier-1 secrets. Encrypt at rest. See Section 7.

Lock-take, modify, lock-release

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

6. Performance

Where the time goes

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

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

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

Bulk operations are non-negotiable for scale

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

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

refreshall strategies

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

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

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

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

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

Avoid re-instantiation churn

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

Panorama scaling

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

Note

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


7. Security

Credential handling

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

Three rules:

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

Warning

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

TLS verification

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

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

Caution

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

Protecting exported configurations

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

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

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

What NOT to log

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

Warning

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

Authorization model

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

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

Network exposure

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


8. Developer Internals

Repository layout

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

The metaclass / object-tree machinery

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

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

The classic shape of a VersionedPanObject subclass:

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

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

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

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

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

Exception hierarchy

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

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

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

Wrapping pan.xapi.PanXapi

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

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

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

Testing strategy

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

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

Adding a new class

The standard recipe to add a new firewall config type:

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

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

Contributing

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


9. Integration Points

What pan-os-python calls into

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

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

What might call into pan-os-python

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

Tip

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

Plugin ecosystem (Prisma Access / Cloud Services)

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

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

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

User-ID integration

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

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

Note

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

Predefined-objects integration

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

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

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

Software / content updates

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

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

Caution

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

CI/CD example

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

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