Palo Alto Ansible Example - Interfaces and Zones

In the previous post, we covered Ansible + Palo Alto fundamentals, in this post, let's go over the example of how to create Interfaces and Zones using a simple Ansible playbook.

Overview

Recently, I came across the task of preparing the configurations for a new Palo Alto firewall deployment. The initial configurations include creating port-channels, subinterfaces, zones and IP addresses. I habitually prepare the configuration in advance and then deploy it to the device rather than creating them one by one in the web GUI.

There are many ways you can do this such as using REST API, set commands, Python or Ansible. I've decided to use Ansible for this task as it has out-of-the-box modules for the resources.

Our goal here is to create aggregate interfaces (port-channel), sub-interfaces, IP addresses and Zones using Ansible. The playbook will deploy the configuration to the Panorama.

💡
If you don't use Panorama and want to configure the firewall directly, exclude the templateparameter from the playbook. 

File Structure

The example is based on the Palo Alto Ansible Collection https://paloaltonetworks.github.io/pan-os-ansible/

Let's look at the file structure and the contents of each file.

.
├── ansible.cfg
├── host_file
├── interface_var.yml
└── playbook.yml

0 directories, 4 files

Ansible Config File - ansible.cfg

[defaults]
inventory = host_file

The default ansible.cfgfile is located in /etc/ansibledirectory. I created a new one just for this example and added the path to the inventoryfile that is shown below.

Inventory File - host_file

The configuration will be pushed down to the Panorama listed in the host_file inventory file. I've created a group called panorama which we will reference in the playbook so, Ansible knows which remote device it should run the playbook against.

[panorama]
panorama-01 ansible_host=10.15.60.10

Ansible Playbook - playbook.yml

This is the entire playbook which we will break down in the next section.

- name: Palo Alto Playbook
  hosts: panorama
  gather_facts: false
  connection: local
  vars_files:
      - "interface_var.yml"

  collections:
    - paloaltonetworks.panos

  vars:
    ansible_python_interpreter: /usr/local/bin/python
    provider:
      ip_address: '10.15.60.10'
      api_key: <YOUR_API_KEY>

  tasks:
    - name: Create aggregate interfaces
      panos_aggregate_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item.name }}"
        vsys: vsys1
      with_items:
        - name: ae1
        - name: ae2

    - name: Create ethernet interfaces and add them to ae1
      panos_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item }}"
        mode: aggregate-group
        aggregate_group: ae1
      with_items:
        - ethernet1/13
        - ethernet1/14
    
    - name: Create ethernet interfaces and add them to ae2
      panos_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item }}"
        mode: aggregate-group
        aggregate_group: ae2
      with_items:
        - ethernet1/15
        - ethernet1/16
    
    - name:  Create Subinterfaces
      panos_l3_subinterface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        enable_dhcp: false
        name: "{{ item.id }}"
        zone_name: "{{ item.zone }}"
        comment: "{{ item.name }}"
        tag: "{{ item.vlan }}"
        ip: "{{ item.ip }}"
        vsys: "{{ item.vsys }}"
      with_items: "{{ interfaces }}"
    
    - name: Zone Configurations
      panos_zone:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        zone: "{{ item.name }}"
        mode: 'layer3'
        interface: "{{ item.interface }}"
        zone_profile: "{{ item.zone_profile }}"
      with_items: "{{ zones }}"

Variable File - interface_var.yml

We could have added all the variables inside the Playbook itself but my preference is to de-couple them and keep them in a  separate file.

---
interfaces:
  - name: ISP-1
    vlan: 5
    id: ae1.5
    ip: 101.10.12.2/30
    zone: WAN-ZONE
    vsys: vsys1
  - name: ISP-2
    vlan: 6
    id: ae1.6
    ip: 185.10.12.2/30
    zone: WAN-ZONE
    vsys: vsys1
  - name: USERS
    vlan: 10
    id: ae2.10
    ip: 10.26.10.1/24
    zone: USERS-ZONE
    vsys: vsys1
  - name: APPLICATIONS
    vlan: 11
    id: ae2.11
    ip: 10.26.11.1/24
    zone: APP-ZONE
    vsys: vsys1
  - name: DMZ
    vlan: 12
    id: ae2.12
    ip: 10.26.12.1/24
    zone: DMZ-ZONE
    vsys: vsys1

zones:
  - name: WAN-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae1.5
      - ae1.6
  - name: USERS-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.10
  - name: APP-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.11
  - name: DMZ-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.12

Let's break it down block by block. The first three blocks are pre-requisite to run the actual tasks.

- name: Palo Alto Playbook
  hosts: panorama
  gather_facts: false
  connection: local

name is self-explanatory, we are defining the name of the play. hosts defines the devices the playbook should run against.

By default, Ansible will gather facts from the remote devices. Facts include hardware model, serial number, interfaces, IPs etc. I set it to false as I don't intend to use the facts for this example.

vars_files:
  - "interface_var.yml"

var_files is quite important, you can define variables in reusable variables files instead of defining them directly in the playbook. When you define variables in reusable variable files, the variables are separated from playbooks. This separation enables you to store your playbooks in source control software (Github) and even share the playbooks, without the risk of exposing any sensitive information. In this example, the variable file contains zone names and IP addresses that are separated from the actual playbook.  

  collections:
    - paloaltonetworks.panos

In our playbooks, we also need to specify that we want to use the panos collection by adding the above lines.

vars:
  ansible_python_interpreter: /usr/local/bin/python
  provider:
    ip_address: '10.15.60.10'
    api_key: <YOUR_API_KEY>

Every task in the playbook requires to have the provider parameter. The provider contains the IP address and the API key as a minimum.  


Tasks

Task-1 Create Aggregate Interfaces

Our first task is to create two aggregate interfaces. Instead of creating each interface via a separate task, we can use Ansible loop to iterate over a list of items as shown below.

Please note for the first three tasks, I've defined the variables directly in the playbook using with_items. The rest of the tasks obtain the variables from the varfile.

  tasks:
    - name: Create aggregate interfaces
      panos_aggregate_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item.name }}"
        vsys: vsys1
      with_items:
        - name: ae1
        - name: ae2

Tasks-2,3 Add physical interfaces to aggregate interfaces

The next two tasks add physical interfaces into the ae interfaces we've created in the previous steps.

    - name: Create ethernet interfaces and add them to ae1
      panos_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item }}"
        mode: aggregate-group
        aggregate_group: ae1
      with_items:
        - ethernet1/13
        - ethernet1/14
    
    - name: Create ethernet interfaces and add them to ae2
      panos_interface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        if_name: "{{ item }}"
        mode: aggregate-group
        aggregate_group: ae2
      with_items:
        - ethernet1/15
        - ethernet1/16

Task-4 Creating Sub-Interfaces

The next step is to create sub-interfaces and assign the correct values to them such as zone, comment, IP address and vsys. As I mentioned before, the variables are stored in a different file called interface_var.yml

The playbook will iterate through the entire list and configure them individually.

    - name:  Create Subinterfaces
      panos_l3_subinterface:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        enable_dhcp: false
        name: "{{ item.id }}"
        zone_name: "{{ item.zone }}"
        comment: "{{ item.name }}"
        tag: "{{ item.vlan }}"
        ip: "{{ item.ip }}"
        vsys: "{{ item.vsys }}"
      with_items: "{{ interfaces }}"
interfaces:
  - name: ISP-1
    vlan: 5
    id: ae1.5
    ip: 101.10.12.2/30
    zone: WAN-ZONE
    vsys: vsys1
  - name: ISP-2
    vlan: 6
    id: ae1.6
    ip: 185.10.12.2/30
    zone: WAN-ZONE
    vsys: vsys1
  - name: USERS
    vlan: 10
    id: ae2.10
    ip: 10.26.10.1/24
    zone: USERS-ZONE
    vsys: vsys1
  - name: APPLICATIONS
    vlan: 11
    id: ae2.11
    ip: 10.26.11.1/24
    zone: APP-ZONE
    vsys: vsys1
  - name: DMZ
    vlan: 12
    id: ae2.12
    ip: 10.26.12.1/24
    zone: DMZ-ZONE
    vsys: vsys1

Task-4 Zones

The final step is to configure zone-related parameters such as zone protection profiles and interfaces.  

Please note that the zones were already created with task 4, this step is just to add zone-specific parameters.

    - name: Zone Configurations
      panos_zone:
        provider: '{{ provider }}'
        template: 'hq-firewall'
        zone: "{{ item.name }}"
        mode: 'layer3'
        interface: "{{ item.interface }}"
        zone_profile: "{{ item.zone_profile }}"
      with_items: "{{ zones }}"
zones:
  - name: WAN-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae1.5
      - ae1.6
  - name: USERS-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.10
  - name: APP-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.11
  - name: DMZ-ZONE
    zone_profile: default_zone_protect
    interface:
      - ae2.12

Run the Playbook

It's time to run the playbook, 'yellow' indicates that Ansible is making changes to the device aka a new configuration has been added.

Verification

You can verify the configurations by navigating to the GUI. As you can see below, the configurations are added correctly. We just need to commit the changes via Panorama.

Ansible Idempotent

Wikipedia defines idempotent as 'Idempotence is the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application'.

Majority of the Network Modules support idempotence meaning you can run the same playbook over and over again and it will not make any changes to the device.

As you can see below, I re-ran the playbook again and the output is shown in 'green' and the bottom of the line says ok=5    changed=0 which indicates none of the configurations on the Palo Alto was changed.

Closing up

As I mentioned before, there are many ways to achieve the end result but my preference has always been Ansible. If I need to deploy another firewall in the future, all I had to do is change the specific attributes from the var file such as interface name, zone name, IP address and etc.

References

https://paloaltonetworks.github.io/pan-os-ansible/