Nautobot - Palo Alto how to keep up with environment? - Part 1

22 March 2026

Let's take a look how we can synchronise what we already have deployed with Nautobot.

As I mentioned in my previous blog post, Nautobot is a robust and flexible tool that can be customized in many different ways. In this entry I would like to share with you how I'm using Nautobot to keep track of my Palo Alto environment. This solution is inspired by my mentor and friend João Soares, from whom I learned a lot about network automation and general approach to modern operations.

Panorama?

Imagine a situation where you already have a Palo Alto environment — it's doing fine, settled down on the perimeter, doing its work for quite some time. There is a number of engineers taking care of it, making changes, adding new devices, changing configurations. It's a live environment.

You are sitting with your Nautobot instance and want to show how it could help with day-to-day tasks. Your starting point is to build an inventory (let's assume you've already taken care of IPAM). But you don't want to type everything manually, and the environment is a living organism — typing manually will get you behind very quickly.

Ideally you could ask engineers to define new devices in Nautobot and make it intent-based. But you are not at that point yet — you can't just tell engineers to change their workflow because you have a new toy. So let's make Nautobot keep up with the environment for now. And I bet there is Panorama in your setup, otherwise it would be hard to manage that many devices — and you can use it to your advantage.

Panorama!

Palo Alto created a nice PAN-OS SDK for interacting with their devices, and I will be quite often referring to it. Using this package, let's write a Nautobot job that will connect to Panorama, grab all the information about connected devices, and update our Nautobot inventory with it.

What we essentially need to do is extract from Panorama the information we want in Nautobot — in my case: hostname, model, serial number, and of course IP address. I've found that the show devices all command gives all the information I need. However, it's not structured data — it's just a text output. Luckily there is a switch that lets us get it in XML format. Let's extract what's interesting for us:

entries = pano.op("show devices all", cmd_xml=True).findall(".//devices/entry")

That gives us a solid structure to work with.

What gets created in Nautobot for each firewall reported by Panorama:

  • Device — hostname, serial number, status (Active / Offline based on Panorama connected state)
  • Device Type — auto-created from the model name (e.g. PA-440, PA-3420) under manufacturer Palo Alto
  • PlatformPAN-OS, shared across all devices
  • Role — Firewall
  • Management interface — named management, flagged as mgmt-only
  • IP address/32 host address assigned to the management interface and set as primary IPv4
  • Software version — e.g. 12.1.4-h2 scoped to the PAN-OS platform, created if it doesn't exist yet

Location resolution is determined in priority order:

  • The parent IP prefix in Nautobot — if the prefix has a location assigned, the device inherits it
  • Fallback — the location of the Panorama device itself

Safety features:

  • Each device is processed in an atomic transaction — if something fails mid-way, that device is fully rolled back and the job moves on to the next one
  • A parent prefix must exist for the device IP before anything is created — no orphaned IPs
  • Devices missing a serial number or model are silently skipped

Nautobot job code

Let's break down the code of the job itself. I won't go into every detail, but it's worth to mention that there is device type/model library from where you can import data. Let's dive to some code highlights:

Staging the common objects we need for all devices — manufacturer, platform, role, and statuses:

manufacturer, _ = Manufacturer.objects.get_or_create(name="Palo Alto")
platform, _ = Platform.objects.get_or_create(name="PAN-OS", defaults={"manufacturer": manufacturer})
role, _ = Role.objects.get_or_create(name="Firewall", defaults={"color": "ff0000"})
role.content_types.add(ContentType.objects.get_for_model(Device))

active = Status.objects.get(name="Active")
offline = Status.objects.get(name="Offline")
namespace = Namespace.objects.get(name="Global")

Stripping down xml entries to the information we want, and skipping devices that don't have a serial number or model name (we don't want to create incomplete records in Nautobot):

for entry in entries:
        serial = entry.findtext("serial", "").strip()
        hostname = entry.findtext("hostname", "").strip()
        model_name = entry.findtext("model", "").strip()
        ip_addr = entry.findtext("ip-address", "").strip()

        if not serial or not model_name:
            continue

Location resolution logic:

location = None
if ip_addr:
    prefix = Prefix.objects.filter(
        namespace=namespace, network__net_contains=f"{ip_addr}/32"
    ).order_by("-prefix_length").first() 
    if not prefix:
        self.logger.error("No parent prefix for %s – skipping %s", ip_addr, hostname)
        continue
    location = prefix.location

if not location:
    location = panorama_device.location

And now the main part — creating/updating the device and related objects in Nautobot. Note that we wrap everything in a transaction, so if anything fails during processing of a single device, it will be rolled back and won't affect the rest of the devices.

with transaction.atomic():
    device_type, _ = DeviceType.objects.get_or_create(model=model_name, manufacturer=manufacturer)

    device, created = Device.objects.update_or_create(
        serial=serial,
        defaults={
            "name": hostname,
            "device_type": device_type,
            "role": role,
            "platform": platform,
            "status": active if entry.findtext("connected") == "yes" else offline,
            "location": location,
        },
    )
    self.logger.info("%s device", "Created" if created else "Updated", extra={"object": device})

    if ip_addr:
        mgmt, _ = Interface.objects.get_or_create(
            device=device, name="management", defaults={"type": "other", "status": active, "mgmt_only": True}
        )
        ip_obj, _ = IPAddress.objects.get_or_create(
            address=f"{ip_addr}/32", namespace=namespace, defaults={"status": active}
        )
        IPAddressToInterface.objects.get_or_create(ip_address=ip_obj, interface=mgmt)
        device.primary_ip4 = ip_obj

    sw_ver = entry.findtext("sw-version", "").strip()
    if sw_ver:
        device.software_version, _ = SoftwareVersion.objects.get_or_create(
            version=sw_ver, platform=platform, defaults={"status": active}
        )

    device.validated_save()

You can check out the whole code on my GitHub.

Remember to schedule the job to run regularly — of course depends on how often your environment changes, for some environments onve a week could be enough, for others you might want to run it daily.

Conclusion

This job is a great starting point for keeping your Nautobot inventory in sync with your Palo Alto environment.

In the next part of this blog post, I will show you how to extend this job to enrich the data in Nautobot in a customized way based on tags that can be set up for devices in Panorama.

Stay tuned!