Automation with Ansible – Palo Alto/Panorama address object creation

Introduction

A while back, I begun my journey towards picking up some automation skills and wanted something that had (relatively speaking) low barrier to entry so that I could begin using it sooner than later. Ansible fit the bill for me.

For my first full-featured script, I wanted to see if I could tackle a seemingly benign, but rather time consuming and chaotic situations that I’ve run into while dealing with duplicate address objects. For instance, consider an IP address of 4.2.2.2. Three different address objects with the same IP address are created as – ‘h-4.2.2.2’ , ‘h_4.2.2.2_32’ and ‘web-4.2.2.2’. These duplicate objects are then referenced in multiple policies. This is what it might look like in a central management server:

Policy IDSourceDestinationPortActionFirewalls applied
1x.x.x.x/24h-4.2.2.2443allowfirewall-a, firewall-b and firewall-c
2x.x.x.x/24h_4.2.2.2_32443allowfirewall-a and firewall-c
3x.x.x.x/24web-4.2.2.2443allowfirewall-b and firewall-c
Firewall policy example from a centralized management server

When you get a call saying that users cannot get to 4.2.2.2 over TCP443, which policy are you going to start looking at – 1st, 2nd or 3rd? Picture this is in a environment with 10s or 100s of firewalls. There’s going to be an exponential increase in the number of redundant, overlapping policies – ultimately leading to:

  • Unnecessary time spent in trying to figure out the matching policy.
  • Longer resolution times – time is money!!
  • Tiresome cleanup – an unwanted policy can’t be removed if it is referencing a redundant address object.

Flowchart

Before getting started with the playbook, I created a flowchart to help me visualize what I wanted this script to achieve.

Essentially, the Playbook should:

  • Take our input of either single IP, network, range or FQDN
  • Compare it with existing address objects
  • Create new objects.

With this framework in mind, let’s get started. Click here, if you want to skip over to final playbook.

Plays

Play1

Here’s the 1st section of our playbook. Key components of this ‘Play’ include:

  • Device type as defined in our inventory .ini file (hosts).
  • Panos collections (which is Ansible content and modules packaged by various vendor, in this case Palo Alto).
  • Device credentials (which in my environment is defined in the host_vars directory).
  • Bunch of variables initialized to null list i.e [].
  • And a screen prompt for user to enter the csv file that contains addresses, subnets, ranges or FQDNs.
---
- name: create non-duplicate address objects
  hosts: pan
  gather_facts: False
  collections:
    - paloaltonetworks.panos
  vars:
    pan_creds:
        ip_address: "{{ ip_address }}"
        username: "{{ username }}"
        password: "{{ password }}"
    found_ip_list: []
    found_subnet_list: []
    found_range_list: []
    found_fqdn_list: []
    nonexistent_host_list: []
    nonexistent_subnet_list: []
    nonexistent_range_list: []
    nonexistent_fqdn_list: []

  vars_prompt: 
    - name: file_name
      prompt: "Enter file name where objects are stored"
      private: no

Tasks

Task1-5

The next few code snippets form the meat of our playbook. Let’s break this down and focus on a few tasks at a time.

Tasks1-5
  • We start by using ‘panos_object_facts’ module packaged within the Palo Alto collection to extract all the address objects present in Panorama.
  • Map() is a jinja2 filter, that is used to extract the content of ‘value’ from the list returned in task 1 and assign those to a variable named ‘pan_ip‘.
  • Task 3 and 4 points to our address file and extracts the address objects under source and destination column (Note: You may have to slightly modify this to reflect the way access is requested in your environment).
  • Finally, we use set theory to create a new list of objects named ‘new_ip’, that we may to need to create.
 tasks:
    - name: retrieve address objects
      panos_object_facts:
        provider: "{{ pan_creds }}"
        name_regex: '.*.*'
        object_type: 'address'
      register: address_objects
      tags: always

    - name: create a list of existing ip addresses
      set_fact:
        pan_ip: "{{ address_objects.objects | map(attribute = 'value') | list }}"
      tags: always

    - name: reading from csv
      read_csv:
        path: "{{ file_name }}"
        delimiter: ","
      register: frr_csv
      tags: always

    - name: extract source and destination field from csv
      set_fact:
        frr_src: "{{ frr_csv.list | map(attribute='Source') | list }}"
        frr_dest: "{{ frr_csv.list | map(attribute='Destination') | list }}"
      tags: always      
  
    - name: create a combined single list of sources and destinations
      set_fact:
        new_ip: "{{ frr_src | union(frr_dest) }}"
      tags: always

I’ve pasted the contents of the csv file. in case you are wondering what it looks like.

Protocol,Source,Destination,Port,BusinessJustification
TCP,10.226.157.0/25,10.72.162.0/24,2009,File Replication
UDP,0.0.0.0/0,10.72.163.0/25,2049,Allow any internal client to access NFS shares.
UDP,www.test123.com,123.abc-123.co.in,111,Allow port 111.
UDP,10.72.162.1-10.72.162.100,4.2.2.2,53,Allow DNS.
TCP,4.2.3.4,10.226.157.200-10.226.157.209,22,Permit SSH.
TCP,4.2.3.4,10.226.157.200-10.226.157.209,636,Allow LDAPs.
TCP,abc-123.co.in,www.test123.com,443,Allow HTTPs.
Tasks6-11
  • With the help of the help of ‘ipaddr’ jinja2 filter, we create new, separate lists of hosts and subnets (from the lists obtained .in tasks 2 and task 5).
  • We compare these lists with each other (i.e existing hosts list in Panorama v/s new hosts from csv AND existing subnets list in Panorama v/s new subnets from csv).
  • The resulting lists – ‘nonexistent_host_list‘ and ‘nonexistent_subnet_list‘ are ultimately what will be committed to Panorama.
  • Extracting network ranges and FQDNs require a bit of regex as I could not locate jinja2 filters to extract the same. This is discussed in next few tasks.
    - name: create new list of only hosts using ipaddr filter
      set_fact:
        pan_ip_host: "{{ pan_ip | ipaddr('host') }}"
        new_ip_host: "{{ new_ip | ipaddr('host') }}"
      tags: always

    - name: get list of ip hosts that match existing hosts
      set_fact:
        found_host_list: "{{ found_host_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_host }}"
      loop: "{{ new_ip_host }}"
      tags: check

    - name: get list of unique ip hosts that do not already exist in pan
      set_fact:
        nonexistent_host_list: "{{ nonexistent_host_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_host }}"
      loop: "{{ new_ip_host }}"
      tags: always

    - name: create new list of only networks using ipaddr filter
      set_fact:
        pan_ip_net: "{{ pan_ip | ipaddr('net') }}"
        new_ip_net: "{{ new_ip | ipaddr('net') }}"
      tags: always

    - name: get list of subnets that match existing subnets
      set_fact:
        found_subnet_list: "{{ found_subnet_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_net }}"
      loop: "{{ new_ip_net }}" 
      tags: check

    - name: get list of subnets that do not already exist in pan
      set_fact:
        nonexistent_subnet_list: "{{ nonexistent_subnet_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_net }}"
      loop: "{{ new_ip_net }}"
      tags: always
Tasks12-19
  • We again extract and create separate lists for network ranges and FQDNs, using ‘regex_findall’ filter.
  • Let’s take a closer look at the expression “\b\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}\b-\b\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}\b”.
    • \b matches at the beginning or end of a string.
    • \d looks for any number 0-9.
    • {1,3} looks for 1 to 3 occurrences of the proceeding pattern i.e 0-9.
    • This pattern is repeated 4 times, followed by a hyphen and then repeated 4 more times.
  • Now let’s look at the expression for finding FQDNs “/^.+\d+\D+$/gm”
    • ^ begins search at start of a line
    • .+ searches for one or more occurrences of any character
    • \d looks for any digit
    • \D matches a non-digit character
    • + looks for at least one to unlimited matches of the preceding expression
    • Finally, $ signifies end of a line
    - name: create existing_ip_range list of only ranges using regex
      set_fact:
        pan_ip_range: '{{ pan_ip_range | default ([]) + (item | regex_findall("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b\-\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")) }}'
      loop: "{{ pan_ip }}"
      tags: always

    - name: create new_ip_range list of only ranges using regex
      set_fact:
        new_ip_range: '{{ new_ip_range | default([]) + (item | regex_findall("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b\-\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")) }}'
      loop: "{{ new_ip }}"
      tags: always

    - name: get list of ranges that match existing ranges
      set_fact:
        found_range_list: "{{ found_range_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_range }}"
      loop: "{{ new_ip_range }}"
      tags: check

    - name: get list of ranges that do not already exist in pan
      set_fact:
        nonexistent_range_list: "{{ nonexistent_range_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_range }}"
      loop: "{{ new_ip_range }}"
      tags: always

    - name: create list of only fqdns found in pan using regex
      set_fact:
        pan_fqdn: "{{ pan_fqdn | default ([]) + [item] }}"
      when: '(item | regex_findall("^.+\d+\D+$"))'
      loop: "{{ pan_ip }}"
      tags: always

    - name: create list of only fqdns from user entered list
      set_fact:
        new_fqdn: "{{ new_fqdn | default([]) + [item] }}"
      when: '(item | regex_findall("^.+\d+\D+$"))'
      loop: "{{ new_ip }}"
      tags: always

    - name: get list of fqdns that matching existing fqdns in pan
      set_fact:
        found_fqdn_list: "{{ found_fqdn_list | default([]) + [item] }}"
      when: "item in pan_fqdn"
      loop: "{{ new_fqdn }}"
      tags: check

    - name: get list of fqnds that do not already exist in pan
      set_fact:
        nonexistent_fqdn_list: "{{ nonexistent_fqdn_list | default ([]) + [item] }}"
      when: "item not in pan_fqdn"
      loop: "{{ new_fqdn }}"
      tags: always
Task20-21
  • We then display the various lists from the previous tasks.
  • When this script is run along with the ‘check‘ tag, it helps give an idea of what will be deployed without having to actually commit anything to Panorama. Kind of like a dry run.
 - name: print hosts, networks and ranges that are already existing
      debug:
        msg:
          - "These objects exist in pan and can be ignored from your deployment"
          - "Existing ip hosts: {{ found_host_list }}"
          - "Existing ip subnets: {{ found_subnet_list }}"
          - "Existing ip ranges: {{ found_range_list }}"
          - "Existing fqdns: {{ found_fqdn_list }}"
      tags: check

    - name: print hosts, networks and ranges that do not exist in the pan
      debug:
        msg:
          - "These objects do NOT exist in pan and will be added should you choose to deploy"
          - "Non-existent ip hosts: {{ nonexistent_host_list }}"
          - "Non-existent ip subnets: {{ nonexistent_subnet_list }}"
          - "Non-existent ip ranges: {{ nonexistent_range_list }}"
          - "Non-existent fqdns: {{ nonexistent_fqdn_list }}" 
      tags: check
Task22-26
  • We finish off our script by deploying the lists of various address objects.
  • The ‘panos_address_object’ module helps create address objects.
  • If you notice the ‘name’ parameter within the tasks, you will see that we are creating objects whose names are reflective of the type of address they are.
  • For instance:
    • individual host: h_1.2.3.4
    • subnet: n_1.2.3.0
    • range: r_1.2.3.4-1.2.3.11
    • fqdn: fqdn_www.123test.co.xyz
  • Our address objects all have standardized and intuitive names!
    - name: deploy host ip address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "h_{{ item.strip('/32') }}"
        address_type: "ip-netmask"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false 
      loop: "{{ nonexistent_host_list }}"
      tags: deploy

    - name: deploy subnet address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: 'n_{{ item | regex_replace("/\d+","") }}'
        address_type: "ip-netmask"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_subnet_list }}"
      tags: deploy

    - name: deploy range address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "r_{{ item }}"
        address_type: "ip-range"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_range_list }}"
      tags: deploy

    - name: deploy fqdn address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "fqdn_{{ item[0:57] }}"
        address_type: "fqdn"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_fqdn_list }}"
      tags: deploy

    - name: commit config
      panos_commit:
        provider: "{{ pan_creds }}"
      tags: deploy

Playbook

Here’s the entire playbook

---
- name: create non-duplicate address objects
  hosts: pan
  gather_facts: False
  collections:
    - paloaltonetworks.panos
  vars:
    pan_creds:
        ip_address: "{{ ip_address }}"
        username: "{{ username }}"
        password: "{{ password }}"
    found_ip_list: []
    found_subnet_list: []
    found_range_list: []
    found_fqdn_list: []
    nonexistent_host_list: []
    nonexistent_subnet_list: []
    nonexistent_range_list: []
    nonexistent_fqdn_list: []
  
  vars_prompt: 
    - name: file_name
      prompt: "Enter file name where objects are stored"
      private: no
  
  tasks:
    - name: retrieve address objects
      panos_object_facts:
        provider: "{{ pan_creds }}"
        name_regex: '.*.*'
        object_type: 'address'
      register: address_objects
      tags: always

    - name: create a list of existing ip addresses
      set_fact:
        pan_ip: "{{ address_objects.objects | map(attribute = 'value') | list }}"
      tags: always

    - name: reading from csv
      read_csv:
        path: "{{ file_name }}"
        delimiter: ","
      register: frr_csv
      tags: always

    - name: extract source and destination field from csv
      set_fact:
        frr_src: "{{ frr_csv.list | map(attribute='Source') | list }}"
        frr_dest: "{{ frr_csv.list | map(attribute='Destination') | list }}"
      tags: always      
  
    - name: create a combined single list of sources and destinations
      set_fact:
        new_ip: "{{ frr_src | union(frr_dest) }}"
      tags: always

    - name: create new list of only hosts using ipaddr filter
      set_fact:
        pan_ip_host: "{{ pan_ip | ipaddr('host') }}"
        new_ip_host: "{{ new_ip | ipaddr('host') }}"
      tags: always

    - name: get list of ip hosts that match existing hosts
      set_fact:
        found_host_list: "{{ found_host_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_host }}"
      loop: "{{ new_ip_host }}"
      tags: check

    - name: get list of unique ip hosts that do not already exist in pan
      set_fact:
        nonexistent_host_list: "{{ nonexistent_host_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_host }}"
      loop: "{{ new_ip_host }}"
      tags: always

    - name: create new list of only networks using ipaddr filter
      set_fact:
        pan_ip_net: "{{ pan_ip | ipaddr('net') }}"
        new_ip_net: "{{ new_ip | ipaddr('net') }}"
      tags: always

    - name: get list of subnets that match existing subnets
      set_fact:
        found_subnet_list: "{{ found_subnet_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_net }}"
      loop: "{{ new_ip_net }}" 
      tags: check

    - name: get list of subnets that do not already exist in pan
      set_fact:
        nonexistent_subnet_list: "{{ nonexistent_subnet_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_net }}"
      loop: "{{ new_ip_net }}"
      tags: always

    - name: create existing_ip_range list of only ranges using regex
      set_fact:
        pan_ip_range: '{{ pan_ip_range | default ([]) + (item | regex_findall("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b\-\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")) }}'
      loop: "{{ pan_ip }}"
      tags: always

    - name: create new_ip_range list of only ranges using regex
      set_fact:
        new_ip_range: '{{ new_ip_range | default([]) + (item | regex_findall("\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b\-\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")) }}'
      loop: "{{ new_ip }}"
      tags: always

    - name: get list of ranges that match existing ranges
      set_fact:
        found_range_list: "{{ found_range_list | default ([]) + [item] }}"
      when: "{{ item in pan_ip_range }}"
      loop: "{{ new_ip_range }}"
      tags: check

    - name: get list of ranges that do not already exist in pan
      set_fact:
        nonexistent_range_list: "{{ nonexistent_range_list | default ([]) + [item] }}"
      when: "{{ item not in pan_ip_range }}"
      loop: "{{ new_ip_range }}"
      tags: always

    - name: create list of only fqdns found in pan using regex
      set_fact:
        pan_fqdn: "{{ pan_fqdn | default ([]) + [item] }}"
      when: '(item | regex_findall("\D+$"))'
      loop: "{{ pan_ip }}"
      tags: always

    - name: create list of only fqdns from user entered list
      set_fact:
        new_fqdn: "{{ new_fqdn | default([]) + [item] }}"
      when: '(item | regex_findall("\D+$"))'
      loop: "{{ new_ip }}"
      tags: always

    - name: get list of fqdns that matching existing fqdns in pan
      set_fact:
        found_fqdn_list: "{{ found_fqdn_list | default([]) + [item] }}"
      when: "item in pan_fqdn"
      loop: "{{ new_fqdn }}"
      tags: check

    - name: get list of fqnds that do not already exist in pan
      set_fact:
        nonexistent_fqdn_list: "{{ nonexistent_fqdn_list | default ([]) + [item] }}"
      when: "item not in pan_fqdn"
      loop: "{{ new_fqdn }}"
      tags: always

    - name: print hosts, networks and ranges that are already existing
      debug:
        msg:
          - "These objects exist in pan and can be ignored from your deployment"
          - "Existing ip hosts: {{ found_host_list }}"
          - "Existing ip subnets: {{ found_subnet_list }}"
          - "Existing ip ranges: {{ found_range_list }}"
          - "Existing fqdns: {{ found_fqdn_list }}"
      tags: check

    - name: print hosts, networks and ranges that do not exist in the pan
      debug:
        msg:
          - "These objects do NOT exist in pan and will be added should you choose to deploy"
          - "Non-existent ip hosts: {{ nonexistent_host_list }}"
          - "Non-existent ip subnets: {{ nonexistent_subnet_list }}"
          - "Non-existent ip ranges: {{ nonexistent_range_list }}"
          - "Non-existent fqdns: {{ nonexistent_fqdn_list }}" 
      tags: check

    - name: deploy host ip address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "h_{{ item.strip('/32') }}"
        address_type: "ip-netmask"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false 
      loop: "{{ nonexistent_host_list }}"
      tags: deploy

    - name: deploy subnet address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: 'n_{{ item | regex_replace("/\d+","") }}'
        address_type: "ip-netmask"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_subnet_list }}"
      tags: deploy

    - name: deploy range address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "r_{{ item }}"
        address_type: "ip-range"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_range_list }}"
      tags: deploy

    - name: deploy fqdn address objects
      panos_address_object:
        provider: "{{ pan_creds }}"
        name: "fqdn_{{ item[0:57] }}"
        address_type: "fqdn"
        value: "{{ item }}"
        description: "created using Ansible via CHG00101"
        commit: false
      loop: "{{ nonexistent_fqdn_list }}"
      tags: deploy

    - name: commit config
      panos_commit:
        provider: "{{ pan_creds }}"
      tags: deploy

Verification

  • Let’s run this script and see if it does what we want it to
ansible-playbook search_addr_obj_demo_v3.yml
Enter file name where objects are stored: /frr_files/frr1.csv

PLAY [create non-duplicate address objects] *****************************************************************************************************************************

TASK [retrieve address objects] *****************************************************************************************************************************************
ok: [pan1]

TASK [create a list of existing ip addresses] **************************************************************************************************************************
ok: [pan1]

TASK [reading from csv] ************************************************************************************************************************************************
ok: [pan1]

TASK [extract source and destination field from csv] *******************************************************************************************************************
ok: [pan1]

TASK [create a combined single list of sources and destinations] *******************************************************************************************************
ok: [pan1]

TASK [create new list of only hosts using ipaddr filter] ***************************************************************************************************************
ok: [pan1]

TASK [get list of ip hosts that match existing hosts] ******************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item in pan_ip_host }}
skipping: [pan1] => (item=4.2.3.4/32)
ok: [pan1] => (item=4.2.2.2/32)

TASK [get list of unique ip hosts that do not already exist in pan] ****************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item not in pan_ip_host }}
ok: [pan1] => (item=4.2.3.4/32)
skipping: [pan1] => (item=4.2.2.2/32)

TASK [create new list of only networks using ipaddr filter] ************************************************************************************************************
ok: [pan1]

TASK [get list of subnets that match existing subnets] *****************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item in pan_ip_net }}
skipping: [pan1] => (item=10.226.157.0/25)
skipping: [pan1] => (item=0.0.0.0/0)
skipping: [pan1] => (item=10.72.162.0/24)
skipping: [pan1] => (item=10.72.163.0/25)

TASK [get list of subnets that do not already exist in pan] ************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item not in pan_ip_net }}
ok: [pan1] => (item=10.226.157.0/25)
ok: [pan1] => (item=0.0.0.0/0)
ok: [pan1] => (item=10.72.162.0/24)
ok: [pan1] => (item=10.72.163.0/25)

TASK [create existing_ip_range list of only ranges using regex] *******************************************************************************************************
ok: [pan1] => (item=4.2.2.2/32)
ok: [pan1] => (item=8.8.8.8/32)
ok: [pan1] => (item=123.abc-123.co.in)

TASK [create new_ip_range list of only ranges using regex] *************************************************************************************************************
ok: [pan1] => (item=10.226.157.0/25)
ok: [pan1] => (item=0.0.0.0/0)
ok: [pan1] => (item=www.test123.com)
ok: [pan1] => (item=10.72.162.1-10.72.162.100)
ok: [pan1] => (item=4.2.3.4)
ok: [pan1] => (item=abc-123.co.in)
ok: [pan1] => (item=10.72.162.0/24)
ok: [pan1] => (item=10.72.163.0/25)
ok: [pan1] => (item=123.abc-123.co.in)
ok: [pan1] => (item=4.2.2.2)
ok: [pan1] => (item=10.226.157.200-10.226.157.209)

TASK [get list of ranges that match existing ranges] *******************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item in pan_ip_range }}
skipping: [pan1] => (item=10.72.162.1-10.72.162.100)
skipping: [pan1] => (item=10.226.157.200-10.226.157.209)

TASK [get list of ranges that do not already exist in pan] *************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item not in pan_ip_range }}
ok: [pan1] => (item=10.72.162.1-10.72.162.100)
ok: [pan1] => (item=10.226.157.200-10.226.157.209)

TASK [create list of only fqdns found in pan using regex] **************************************************************************************************************
skipping: [pan1] => (item=4.2.2.2/32)
skipping: [pan1] => (item=8.8.8.8/32)
ok: [pan1] => (item=123.abc-123.co.in)

TASK [create list of only fqdns from user entered list] ****************************************************************************************************************
skipping: [pan1] => (item=10.226.157.0/25)
skipping: [pan1] => (item=0.0.0.0/0)
ok: [pan1] => (item=www.test123.com)
skipping: [pan1] => (item=10.72.162.1-10.72.162.100)
skipping: [pan1] => (item=4.2.3.4)
ok: [pan1] => (item=abc-123.co.in)
skipping: [pan1] => (item=10.72.162.0/24)
skipping: [pan1] => (item=10.72.163.0/25)
ok: [pan1] => (item=123.abc-123.co.in)
skipping: [pan1] => (item=4.2.2.2)
skipping: [pan1] => (item=10.226.157.200-10.226.157.209)

TASK [get list of fqdns that matching existing fqdns in pan] ***********************************************************************************************************
skipping: [pan1] => (item=www.test123.com)
skipping: [pan1] => (item=abc-123.co.in)
ok: [pan1] => (item=123.abc-123.co.in)

TASK [get list of fqnds that do not already exist in pan] **************************************************************************************************************
ok: [pan1] => (item=www.test123.com)
ok: [pan1] => (item=abc-123.co.in)
skipping: [pan1] => (item=123.abc-123.co.in)

TASK [print hosts, networks and ranges that are already existing] ******************************************************************************************************
ok: [pan1] => {
    "msg": [
        "These objects exist in pan and can be ignored from your deployment",
        "Existing ip hosts: [u'4.2.2.2/32']",
        "Existing ip subnets: []",
        "Existing ip ranges: []",
        "Existing fqdns: [u'123.abc-123.co.in']"
    ]
}

TASK [print hosts, networks and ranges that do not exist in the pan] ***************************************************************************************************
ok: [pan1] => {
    "msg": [
        "These objects do NOT exist in pan and will be added should you choose to deploy",
        "Non-existent ip hosts: [u'4.2.3.4/32']",
        "Non-existent ip subnets: [u'10.226.157.0/25', u'0.0.0.0/0', u'10.72.162.0/24', u'10.72.163.0/25']",
        "Non-existent ip ranges: [u'10.72.162.1-10.72.162.100', u'10.226.157.200-10.226.157.209']",
        "Non-existent fqdns: [u'www.test123.com', u'abc-123.co.in']"
    ]
}

TASK [deploy host ip address objects] **********************************************************************************************************************************
changed: [pan1] => (item=4.2.3.4/32)

TASK [deploy subnet address objects] ***********************************************************************************************************************************
changed: [pan1] => (item=10.226.157.0/25)
changed: [pan1] => (item=0.0.0.0/0)
changed: [pan1] => (item=10.72.162.0/24)
changed: [pan1] => (item=10.72.163.0/25)

TASK [deploy range address objects] ************************************************************************************************************************************
changed: [pan1] => (item=10.72.162.1-10.72.162.100)
changed: [pan1] => (item=10.226.157.200-10.226.157.209)

TASK [deploy fqdn address objects] *************************************************************************************************************************************
changed: [pan1] => (item=www.test123.com)
changed: [pan1] => (item=abc-123.co.in)

TASK [commit config] ***************************************************************************************************************************************************

PLAY RECAP *************************************************************************************************************************************************************
pan1                       : ok=24   changed=5    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0

We observe the new addresses have been configured from the GUI

Nice! In summary, we got Ansible to:

  • Read contents from a ‘csv’ file.
  • Compare it with existing objects and only create new objects.
  • Plus, the address objects are given intuitive names, ensuring that our naming conventions are followed.
  • This standardization further helps in driving future automation efforts.

Thank you all for reading. Would love to hear your suggestions and comments.

References

  • https://paloaltonetworks.github.io/pan-os-ansible/
  • https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html
  • https://jinja.palletsprojects.com/en/3.0.x/templates/#builtin-filters
  • https://www.regular-expressions.info/ip.html
  • https://regex101.com/

2 thoughts on “Automation with Ansible – Palo Alto/Panorama address object creation”

  1. Aw, this was an exceptionally nice post. Spending some time and actual effort
    to make a good article… but what can I say… I procrastinate a whole lot and never seem to get anything done.

Leave a Comment

Your email address will not be published. Required fields are marked *