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.
template
parameter 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.cfg
file is located in /etc/ansible
directory. I created a new one just for this example and added the path to the inventory
file 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 var
file.
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/