Ansible: Using Facts Modules to do Updates

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Announcements
Please sign in to see details of an important advisory in our Customer Advisories area.
L5 Sessionator

So you're using Ansible, and you can add objects easily. You have your playbooks in order and can have no problems adding new configuration to a firewall. But how do you go about updating existing config without having to re-specify everything from scratch?

 

Let's pretend that we have an address object that we want to delete, but before we can delete this address object, we need to remove it from any address groups it might belong to.

 

Before we begin, it's worth noting that although this example is only focusing on address groups, the logic is the same for other areas such as NAT or security rules. Right now, the proper way to get the Palo Alto Networks Ansible modules is using the Ansible Galaxy role, located here.  As of right now, the most recent version of the role is 2.2.2.

 

So, we start out our playbooks all the same way, by making sure to use this role (note that your hosts specification might be different):

 

- name: Network Playbook
  hosts: fw
  connection: local
  gather_facts: false

  roles:
    - role: PaloAltoNetworks.paloaltonetworks

 

Now, let's define a variable that will have the address object we are looking to remove from all address groups. For simplicity's sake, let's define this directly in our playbook. Let's say the address object we are deleting is named ntp2. So let's set this as a variable named rmadr:

 

  vars:
    rmadr: 'ntp2'

 

After that, we need to get the credentials of the firewall we're going to manage. For me, I have the credentials saved in a YAML file named vars.yml that looks like this:

 

aws_provider:
    ip_address: '127.0.0.1'
    username: 'admin'
    password: 'admin'

 

So, to import that, I'll use the include_vars task to pull that into my playbook like this:

 

  tasks:
  - name: Grab auth creds
    include_vars: 'vars.yml'
    no_log: 'yes'

 

Now that I've done all my setup, let's grab all of my address groups.  This is accomplished by using an appropriate facts module, in this case, panos_object_facts:

 

  - name: Get all address groups and their config
    panos_object_facts:
      provider: '{{ aws_provider }}'
      object_type: 'address-group'
      name_regex: '.*'
    register: ag

 

A couple things you'll note about the above:

  • We specify our credentials using the provider dict pulled in from the include_vars task
  • Since we want all of the objects, we just give a regex that will match all address groups
  • We are saving the results of this facts module to a variable named ag
  • Using the documentation for panos_object_facts, we can see that the list of objects are stored in the objects output

Now that we have all of our address groups, we need to remove the address object from all groups that use it. We can accomplish this by using a combination of Jinja2 templating functions as well as Ansible playbook filters, along with the appropriate PAN-OS module.  Let's take this one step at a time.

 

First things first, we will be using the panos_address_group module, so let's start with that:

 

  - name: Remove address object from all groups
    panos_address_group:
      provider: '{{ aws_provider }}'

 

Since we're looping over a list, we will be using the special variable item as the stand-in for param names, so our task config will look like this so far:

 

      name: '{{ item.name }}'
      description: '{{ item.description }}'
      static_value: '{{ item.static_value }}'
      tag: '{{ item.tag }}'
      commit: false

 

Now let's do the address object removal. The static address objects are all saved as a list in static_value, so we'll use difference() to remove it from the static value. Since rmadr is a string and difference() is expecting a list, we will have just make a single list out of the rmadr variable by putting it in square brackets. So we replace the above static_value line with what ends up looking like this:

 

      static_value: '{{ item.static_value | difference([rmadr]) }}'

 

Next let's specify what we're looping over. Remember, we saved the list of address groups to the variable ag, and the output is objects, so let's specify that next:

 

    loop: '{{ ag.objects }}'

 

Now we need to specify the criteria to update address groups. Some care is needed here as there are two types of address groups: static and dynamic, and dynamic address groups don't have a static_value defined at all. So we need to have two loop criteria here: first is that it is actually a static address group to beging with, and second is that the rmadr value should actually be in the static criteria. The first check is necessary as you will get an error if you try to treat a None as a list, the second criteria is there to help speed up overall playbook execution. So it will look like this:

 

    when:
      - item.static_value
      - rmadr in item.static_value

 

Next, we need to handle the None values properly. There might not be a description and there might not be any administrative tags, so we need to say, "if these things aren't set, then don't specify them in the playbook execution." So we'll use default() to do this and pass in the optional second param as true to say, "default against the trutiness of this value, not just if it's defined or not." Updating the previous lines in this task to take this into account, this is what we end up with:

 

      description: '{{ item.description | default(omit, true) }}'
      tag: '{{ item.tag | default(omit, true) }}'

 

Finally, when looping over things, Ansible will by default flood your screen with the exact criteria that the loop is being executed with for each item. We don't need all that. Let's just display the name of the address group instead:

 

    loop_control:
      label: '{{ item.name }}'

 

Then to finish it all up, we need to actually remove the address object. This is straightforward enough:

 

  - name: Finally remove the address object
    panos_address_object:
      provider: '{{ aws_provider }}'
      name: rmadr
      state: 'absent'
      commit: false

 

The final playbook will look like this:

 

- name: Network Playbook
  hosts: fw
  connection: local
  gather_facts: false

  vars:
    rmadr: 'ntp2'

  roles:
    - role: PaloAltoNetworks.paloaltonetworks

  tasks:
  - name: Grab auth creds
    include_vars: 'vars.yml'
    no_log: 'yes'

  - name: Get all address groups and their config
    panos_object_facts:
      provider: '{{ aws_provider }}'
      object_type: 'address-group'
      name_regex: '.*'
    register: ag

  - name: Remove address object from all groups
    panos_address_group:
      provider: '{{ aws_provider }}'
      name: '{{ item.name }}'
      description: '{{ item.description | default(omit, true) }}'
      static_value: '{{ item.static_value | difference([rmadr]) }}'
      tag: '{{ item.tag | default(omit, true) }}'
      commit: false
    loop: '{{ ag.objects }}'
    loop_control:
      label: '{{ item.name }}'
    when:
      - item.static_value
      - rmadr in item.static_value

  - name: Finally remove the address object
    panos_address_object:
      provider: '{{ aws_provider }}'
      name: rmadr
      state: 'absent'
      commit: false

 

For further reading, please refer to Ansible filtering and Jinja2 templating functions.  Happy templating with Ansible!

5 Comments
L0 Member

when i tried to use 

description: '{{ item.description | default(omit, true) }}'

it would send '  | default(omit, true)' instead of omitting it, because it seemed the value is not null but ' '. 

 

This was with the panos_security_rule_facts pulling and me trying to push with the panos_security_rule. ended up using a if/else statement which worked.  

 

wanted to say thanks for the blog -

L0 Member

How would you go about adding / appending a new address object to an existing group?

L0 Member

I figured out how to add a new object to an existing object group, below is a a small snip of my ansible script which run on our Panorama server. Hope this helps

 

vars:   < This is the variable used in the script that creates the object, in the ansible script i also require someone to provide their creds vs static user creds
rds_server_object: x-t-rds01-app-10.x.x.x

cli:
ip_address: "{{ inventory_hostname }}"
username: "{{ cred }}"
password: "{{ creds }}"

roles:
- role: PaloAltoNetworks.paloaltonetworks

tasks:

- name: Gather FW Auth Creds
include_vars: 'cheat_code.yml'  < This yaml file has all the customer specific data, the script uses this data to populate the vars
no_log: 'no'

##### This Section adds rds server object to the Customer RDS group #####


- name: Gathering Facts of the Customer RDS server group
panos_object_facts:
device_group: 'tst-ha-fw'
provider: '{{ cli }}'
object_type: 'address-group'
name_regex: 'Customer RDS-1'
register: customer_rds_group


- name: Adding new customer RDS server to Customer RDS server group
panos_address_group:
device_group: 'tst-ha-fw' <- This is the device group that panorama will be targeting
provider: '{{ cli }}'
name: '{{ item.name }}'
description: '{{ item.description | default(omit, true) }}'
static_value: '{{ item.static_value + [rds_server_object] }}'
tag: '{{ item.tag | default(omit, true) }}'
commit: 'no'
loop: '{{ customer_rds_group.objects }}'
loop_control:
label: '{{ item.name }}'
when:
- item.static_value
- rds_server_object not in item.static_value

L2 Linker

@GFRDA thanks a lot. I cribbed this to edit a rule (removing an object from destination field) and its works a charm!

 

For anyone else here is my playbook:

 

 

- name: Network Playbook
  hosts: panorama
  connection: local
  gather_facts: false

  vars:
    rmadr: 'simon-test-address2'
    myrule: 'REQ0001-RITM00009'

  roles:
    - role: PaloAltoNetworks.paloaltonetworks

  tasks:
  - name: Get auth creds
    include_vars: 'vars.yml'
    #no_log: 'yes'

  - name: Get the definition for rule '{{ myrule }}'
    paloaltonetworks.panos.panos_security_rule_facts:
      provider: '{{ lab_provider }}'
      device_group: 'LAB-VM-virtual-FW-DG'
      rule_name: '{{ myrule | quote }}'
    register: result

  - debug:
      msg: '{{ result }}'

  - name: edit destination for rule '{{ myrule }}' 
    paloaltonetworks.panos.panos_security_rule:
      provider: '{{ lab_provider }}'
      device_group: 'LAB-VM-virtual-FW-DG'
      rule_name: '{{ item.rule_name }}' 
      action: '{{ item.action }}'
      source_zone: '{{ item.source_zone | default(omit, true) }}' 
      destination_zone: '{{ item.destintaion_zone | default(omit, true) }}'
      log_start: 'false'
      log_end: 'true' 
      application: '{{ item.application | default(omit, true) }}' 
      category: '{{ item.category | default(omit, true) }}'
      description: '{{ item.description | default(omit, true) }}'
      destination_ip: '{{ item.destination_ip | difference([rmadr]) }}'
      disabled: '{{ item.disabled }}'       
      source_ip: '{{ item.source_ip | default(omit, true) }}'
      service: '{{ item.service | default(omit, true) }}'
      tag_name: '{{ item.tag | default(omit, true) }}' 
      commit: false
    loop: '{{ result.rule_details }}'
    loop_control:
      label: '{{ item.rule_name }}'

  - name: Get the new definition for rule '{{ myrule }}'
    paloaltonetworks.panos.panos_security_rule_facts:
      provider: '{{ lab_provider }}'
      device_group: 'LAB-VM-virtual-FW-DG'
      rule_name: '{{ myrule | quote }}'
    register: new_result

  - debug:
      msg: '{{ new_result }}'

 

 

Note that "destintaion_zone" is a typo in the tool.

 

And the output when run - I get the rule twice so you can see the difference:

 

 

(python3_9) [admin@host]$ ansible-playbook -i inventory -v remove_address_object_from_source.yml 
No config file found; using defaults

PLAY [Network Playbook] *************************************************************************************************************************************************************

TASK [Get auth creds] ***************************************************************************************************************************************************************
ok: [panorama] => {"ansible_facts": {}

TASK [Get the definition for rule 'REQ0001-RITM00009'] ******************************************************************************************************************************
[DEPRECATION WARNING]: Deprecated; use panos_security_rule with state=gathered instead. This feature will be removed from paloaltonetworks.panos in version 3.0.0. Deprecation 
warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
[DEPRECATION WARNING]: Please use the names parameter instead of rule_name. This feature will be removed from paloaltonetworks.panos in version 3.0.0. Deprecation warnings can be 
disabled by setting deprecation_warnings=False in ansible.cfg.
ok: [panorama] => {"changed": false, "rule_details": [{"action": "allow", "antivirus": null, "application": ["any"], "category": ["any"], "data_filtering": null, "description": "REQ0001", "destination_devices": ["any"], "destination_ip": ["H-192.168.52.1-32", "simon-test-address2"], "destintaion_zone": ["pubinternet"], "disable_server_response_inspection": false, "disabled": true, "file_blocking": null, "group_profile": null, "group_tag": null, "hip_profiles": null, "icmp_unreachable": null, "log_end": true, "log_setting": null, "log_start": false, "negate_destination": false, "negate_source": false, "negate_target": null, "rule_name": "REQ0001-RITM00009", "rule_type": "universal", "schedule": null, "service": ["tcp_443"], "source_devices": ["any"], "source_ip": ["H-8.8.8.8-32"], "source_user": ["any"], "source_zone": ["UNDEFINED"], "spyware": null, "tag_name": null, "target": null, "url_filtering": null, "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77", "vulnerability": null, "wildfire_analysis": null}]}

TASK [debug] ************************************************************************************************************************************************************************
ok: [panorama] => {
    "msg": {
        "changed": false,
        "deprecations": [
            {
                "collection_name": "paloaltonetworks.panos",
                "msg": "Deprecated; use panos_security_rule with state=gathered instead",
                "version": "3.0.0"
            },
            {
                "collection_name": "paloaltonetworks.panos",
                "msg": "Please use the names parameter instead of rule_name.",
                "version": "3.0.0"
            }
        ],
        "failed": false,
        "rule_details": [
            {
                "action": "allow",
                "antivirus": null,
                "application": [
                    "any"
                ],
                "category": [
                    "any"
                ],
                "data_filtering": null,
                "description": "REQ0001",
                "destination_devices": [
                    "any"
                ],
                "destination_ip": [
                    "H-192.168.52.1-32",
                    "simon-test-address2"
                ],
                "destintaion_zone": [
                    "pubinternet"
                ],
                "disable_server_response_inspection": false,
                "disabled": true,
                "file_blocking": null,
                "group_profile": null,
                "group_tag": null,
                "hip_profiles": null,
                "icmp_unreachable": null,
                "log_end": true,
                "log_setting": null,
                "log_start": false,
                "negate_destination": false,
                "negate_source": false,
                "negate_target": null,
                "rule_name": "REQ0001-RITM00009",
                "rule_type": "universal",
                "schedule": null,
                "service": [
                    "tcp_443"
                ],
                "source_devices": [
                    "any"
                ],
                "source_ip": [
                    "H-8.8.8.8-32"
                ],
                "source_user": [
                    "any"
                ],
                "source_zone": [
                    "UNDEFINED"
                ],
                "spyware": null,
                "tag_name": null,
                "target": null,
                "url_filtering": null,
                "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77",
                "vulnerability": null,
                "wildfire_analysis": null
            }
        ]
    }
}

TASK [edit destination for rule 'REQ0001-RITM00009'] ********************************************************************************************************************************
changed: [panorama] => (item=REQ0001-RITM00009) => {"after": {"action": "allow", "antivirus": null, "application": ["any"], "category": ["any"], "data_filtering": null, "description": "REQ0001", "destination_devices": ["any"], "destination_ip": ["H-192.168.52.1-32"], "destination_zone": ["pubinternet"], "disable_server_response_inspection": false, "disabled": true, "file_blocking": null, "group_profile": null, "group_tag": null, "hip_profiles": null, "icmp_unreachable": null, "log_end": true, "log_setting": null, "log_start": false, "negate_destination": false, "negate_source": false, "negate_target": null, "rule_name": "REQ0001-RITM00009", "rule_type": "universal", "schedule": null, "service": ["tcp_443"], "source_devices": ["any"], "source_ip": ["H-8.8.8.8-32"], "source_user": ["any"], "source_zone": ["UNDEFINED"], "spyware": null, "tag_name": null, "target": null, "url_filtering": null, "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77", "vulnerability": null, "wildfire_analysis": null}, "ansible_loop_var": "item", "before": {"action": "allow", "antivirus": null, "application": ["any"], "category": ["any"], "data_filtering": null, "description": "REQ0001", "destination_devices": ["any"], "destination_ip": ["H-192.168.52.1-32", "simon-test-address2"], "destination_zone": ["pubinternet"], "disable_server_response_inspection": false, "disabled": true, "file_blocking": null, "group_profile": null, "group_tag": null, "hip_profiles": null, "icmp_unreachable": null, "log_end": true, "log_setting": null, "log_start": false, "negate_destination": false, "negate_source": false, "negate_target": null, "rule_name": "REQ0001-RITM00009", "rule_type": "universal", "schedule": null, "service": ["tcp_443"], "source_devices": ["any"], "source_ip": ["H-8.8.8.8-32"], "source_user": ["any"], "source_zone": ["UNDEFINED"], "spyware": null, "tag_name": null, "target": null, "url_filtering": null, "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77", "vulnerability": null, "wildfire_analysis": null}, "changed": true, "item": {"action": "allow", "antivirus": null, "application": ["any"], "category": ["any"], "data_filtering": null, "description": "REQ0001", "destination_devices": ["any"], "destination_ip": ["H-192.168.52.1-32", "simon-test-address2"], "destintaion_zone": ["pubinternet"], "disable_server_response_inspection": false, "disabled": true, "file_blocking": null, "group_profile": null, "group_tag": null, "hip_profiles": null, "icmp_unreachable": null, "log_end": true, "log_setting": null, "log_start": false, "negate_destination": false, "negate_source": false, "negate_target": null, "rule_name": "REQ0001-RITM00009", "rule_type": "universal", "schedule": null, "service": ["tcp_443"], "source_devices": ["any"], "source_ip": ["H-8.8.8.8-32"], "source_user": ["any"], "source_zone": ["UNDEFINED"], "spyware": null, "tag_name": null, "target": null, "url_filtering": null, "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77", "vulnerability": null, "wildfire_analysis": null}}

TASK [Get the new definition for rule 'REQ0001-RITM00009'] **************************************************************************************************************************
ok: [panorama] => {"changed": false, "rule_details": [{"action": "allow", "antivirus": null, "application": ["any"], "category": ["any"], "data_filtering": null, "description": "REQ0001", "destination_devices": ["any"], "destination_ip": ["H-192.168.52.1-32"], "destintaion_zone": ["pubinternet"], "disable_server_response_inspection": false, "disabled": true, "file_blocking": null, "group_profile": null, "group_tag": null, "hip_profiles": null, "icmp_unreachable": null, "log_end": true, "log_setting": null, "log_start": false, "negate_destination": false, "negate_source": false, "negate_target": null, "rule_name": "REQ0001-RITM00009", "rule_type": "universal", "schedule": null, "service": ["tcp_443"], "source_devices": ["any"], "source_ip": ["H-8.8.8.8-32"], "source_user": ["any"], "source_zone": ["UNDEFINED"], "spyware": null, "tag_name": null, "target": null, "url_filtering": null, "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77", "vulnerability": null, "wildfire_analysis": null}]}

TASK [debug] ************************************************************************************************************************************************************************
ok: [panorama] => {
    "msg": {
        "changed": false,
        "deprecations": [
            {
                "collection_name": "paloaltonetworks.panos",
                "msg": "Deprecated; use panos_security_rule with state=gathered instead",
                "version": "3.0.0"
            },
            {
                "collection_name": "paloaltonetworks.panos",
                "msg": "Please use the names parameter instead of rule_name.",
                "version": "3.0.0"
            }
        ],
        "failed": false,
        "rule_details": [
            {
                "action": "allow",
                "antivirus": null,
                "application": [
                    "any"
                ],
                "category": [
                    "any"
                ],
                "data_filtering": null,
                "description": "REQ0001",
                "destination_devices": [
                    "any"
                ],
                "destination_ip": [
                    "H-192.168.52.1-32"
                ],
                "destintaion_zone": [
                    "pubinternet"
                ],
                "disable_server_response_inspection": false,
                "disabled": true,
                "file_blocking": null,
                "group_profile": null,
                "group_tag": null,
                "hip_profiles": null,
                "icmp_unreachable": null,
                "log_end": true,
                "log_setting": null,
                "log_start": false,
                "negate_destination": false,
                "negate_source": false,
                "negate_target": null,
                "rule_name": "REQ0001-RITM00009",
                "rule_type": "universal",
                "schedule": null,
                "service": [
                    "tcp_443"
                ],
                "source_devices": [
                    "any"
                ],
                "source_ip": [
                    "H-8.8.8.8-32"
                ],
                "source_user": [
                    "any"
                ],
                "source_zone": [
                    "UNDEFINED"
                ],
                "spyware": null,
                "tag_name": null,
                "target": null,
                "url_filtering": null,
                "uuid": "0a966a5b-66ff-451e-8f74-e1f6f9547c77",
                "vulnerability": null,
                "wildfire_analysis": null
            }
        ]
    }
}

PLAY RECAP **************************************************************************************************************************************************************************
panorama                   : ok=6    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

(python3_9) [admin@host]$ 

 

 

L0 Member

Can you please guide how can I use the above steps to append the new objects in address_group .

 

Also with the way to update the static routes in the router with use of ansible