I really wish Cisco would support the DevOps community and release Ansible modules for their products like most other vendors. That being said, since there are no modules for the Cisco Firepower you have to manage the device through the APIs directly. Managing anything using raw API requests in Ansible can be a little tricky but not impossible. When creating playbooks like this you will typically spend most time figuring out the structure of responses and how best to iterate through them.
The following Ansible playbook is a refactor of a previous script I wrote last year to post/delete objects up to a firepower in bulk. I have spent a lot of time with Ansible playbooks and I recommend grouping and modularizing related tasks into separate importable YAML files. This not only makes reusing common groups of tasks much easier but also means later those logical task groupings can simply be copied up into a role with little to no effort.
main.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text
#!/usr/bin/ansible-playbook -f 10 ## Ansible playbook to manage objects on a FMC # 2019 (v.01) - Playbook from www.davideaves.com --- - name: manage firepower objects hosts: fmc connection: local gather_facts: no vars: - ansible_connection: "local" - ansible_python_interpreter: "/usr/bin/env python" - fmc_provider: username: "{{ username | default('apiuser') }}" password: "{{ password | default('api1234') }}" - fmc_objects: - name: server1 value: 192.0.2.1 description: Test Server tasks: ## Note ## # Firepower Management Center REST API authentication tokens are valid for 30 minutes, and can be refreshed up to three times # Ref: https://www.cisco.com/c/en/us/td/docs/security/firepower/623/api/REST/Firepower_Management_Center_REST_API_Quick_Start_Guide_623/Connecting_with_a_Client.html - name: "fmc_platform: generatetoken" local_action: module: uri url: "https://{{ inventory_hostname }}/api/fmc_platform/v1/auth/generatetoken" method: POST user: "{{ fmc_provider.username }}" password: "{{ fmc_provider.password }}" validate_certs: no return_content: no force_basic_auth: yes status_code: 204 register: auth - include: fmc_objects.yaml when: auth.x_auth_access_token is defined |
The following is the task grouping that will make object changes to the FMC using Ansibles built in URI module. I have tried to make this playbook as idempotent as possible so I first register an array with all of the objects that exist on the FMC. I then iterate through that array in subsequent tasks so I only change what does not match. If it sees a fmc_object name key with no value set, the delete task will remove the object from the FMC.
fmc_objects.yaml: ASCII text
## Cisco FMC object management tasks for Ansible ## Requires: VAR:auth.x_auth_access_token ## 2019 (v.01) - Playbook from www.davideaves.com # ## VARIABLE EXAMPLE ## # # - fmc_objects: # - name: server1 # value: 192.0.2.1 # ## USAGE EXAMPLE ## # - include: fmc_objects.yaml # when: auth.x_auth_access_token is defined # --- ## NOTE ## # Currently only handling host and network objects! # Other object types will likely require a j2 template to construct the body submission. - name: "fmc_config: get all objects" local_action: module: uri url: "https://{{ inventory_hostname }}/api/fmc_config/v1/domain/{{ auth.domain_uuid }}/object/{{ item }}?limit=10000&expanded=true" method: GET validate_certs: no status_code: 200 headers: Content-Type: application/json X-auth-access-token: "{{ auth.x_auth_access_token }}" with_items: - hosts - networks register: "all_objects_raw" # Unable to figure out how to do this without a j2 template. # FMC returns too many subelements to easily filter. - name: "fmc_config: post new objects" local_action: module: uri url: "https://{{ inventory_hostname }}/api/fmc_config/v1/domain/{{ auth.domain_uuid }}/object/{{ fmc_objects | selectattr('name', 'equalto', item) | map(attribute='type') | list | last | default('hosts') | lower }}" method: POST validate_certs: no status_code: 201 headers: Content-Type: application/json X-auth-access-token: "{{ auth.x_auth_access_token }}" body_format: json body: name: "{{ item }}" value: "{{ fmc_objects | selectattr('name', 'equalto', item) | map(attribute='value') | list | last }}" description: "{{ fmc_objects | selectattr('name', 'equalto', item) | map(attribute='description') | list | last | default('Ansible Created') }}" overridable: "{{ fmc_objects | selectattr('name', 'equalto', item) | map(attribute='overridable') | list | last | default('False') | bool }}" with_items: "{{ lookup('template', 'fmc_objects-missing.j2').split('\n') }}" when: (item != "") and (fmc_objects | selectattr('name', 'equalto', item) | map(attribute='value') | list | last is defined) changed_when: True ## NOTE ## # The conditions below will not catch the sudden removal of the description or overridable key - name: "fmc_config: modify existing objects" local_action: module: uri url: "{{ item.1.links.self }}" method: PUT validate_certs: no status_code: 200 headers: Content-Type: application/json X-auth-access-token: "{{ auth.x_auth_access_token }}" body_format: json body: name: "{{ item.1.name }}" id: "{{ item.1.id }}" type: "{{ item.1.type }}" value: "{{ fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='value') | list | last }}" description: "{{ fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='description') | list | last | default('Ansible Created') }}" overridable: "{{ fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='overridable') | list | last | default('False') | bool }}" with_subelements: - "{{ all_objects_raw['results'] }}" - json.items when: (fmc_objects | selectattr('name', 'equalto', item.1.name) | list | count > 0) and (((fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='value') | list | last is defined) and (fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='value') | list | last != item.1.value)) or ((fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='description') | list | last is defined) and (fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='description') | list | last | default('Ansible Created') != item.1.description)) or ((fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='overridable') | list | last is defined) and (fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='overridable') | list | last | default('False') | bool != item.1.overridable))) changed_when: True - name: "fmc_config: delete objects" local_action: module: uri url: "{{ item.1.links.self }}" method: DELETE validate_certs: no status_code: 200 headers: X-auth-access-token: "{{ auth.x_auth_access_token }}" with_subelements: - "{{ all_objects_raw['results'] }}" - json.items when: (fmc_objects | selectattr('name', 'equalto', item.1.name) | list | count > 0) and(fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='name') | list | last is defined) and(fmc_objects | selectattr('name', 'equalto', item.1.name) | map(attribute='value') | list | last is undefined) changed_when: True |
Sometimes when trying to munge an array and perform comparisons you have to do it in a Jinja2 Template. The following template creates a list of existing object names then will check to see if that object needs to be created. This is what my POST task uses to determine what new objects will be created.
templates/fmc_objects-missing.j2: ASCII text
{#- Build a list of the existing objects -#} {% set EXISTING = [] %} {% for object_result in all_objects_raw['results'] %} {% for object_line in object_result['json']['items'] %} {{- EXISTING.append( object_line['name'] ) -}} {% endfor %} {% endfor %} {#- Check fmc_objects to see if missing -#} {% for fmc_object in fmc_objects %} {% if fmc_object['name'] not in EXISTING %} {{ fmc_object['name'] }} {% endif %} {% endfor %} |