I am looking for some feedback/advice/suggestions for my ansible playbook[s]. I included them below in a hopefully useful format. Here is also a directory structure of the files. These playbooks dynamically manage firewall rules in production, so I’m looking for another set of eyes before I push this into production.
.
├── ansible.cfg~
├── docs
│ └── fem_ansible.png
├── fwmgmt.yml
├── inventory
├── README.md
├── roles
│ ├── fortinet_fw
│ │ ├── defaults
│ │ │ └── main.yml
│ │ ├── files
│ │ │ └── iana_reversed.csv
│ │ ├── handlers
│ │ │ └── main.yml
│ │ ├── meta
│ │ │ └── main.yml
│ │ ├── README.md
│ │ ├── tasks
│ │ │ ├── address.yml
│ │ │ ├── main.yml
│ │ │ ├── policy.yml
│ │ │ ├── servicegroup.yml
│ │ │ └── service.yml
│ │ ├── templates
│ │ ├── tests
│ │ │ └── inventory
│ │ └── vars
│ │ └── main.yml
│ └── nb_graphql
│ ├── defaults
│ │ └── main.yml
│ ├── files
│ ├── filter_plugins
│ │ ├── custom_filters.py
│ │ └── __pycache__
│ ├── handlers
│ │ └── main.yml
│ ├── meta
│ │ └── main.yml
│ ├── README.md
│ ├── tasks
│ │ └── main.yml
│ ├── templates
│ ├── tests
│ │ └── inventory
│ └── vars
│ └── main.yml
File: ./fwmgmt.yml
---
- name: Get data from nb
hosts: firewalls
gather_facts: false
vars:
slack_token: "{{ lookup('file', '/opt/firewall/slackkey') }}"
roles:
- role: nb_graphql
- role: fortinet_fw
policy: "{{ rule.json.data.policy_rules[0] }}"
before: "{{ rule.json.data.policy_rules[0].policies[0].policy_rules |
community.general.json_query('[].index')|
find_nearest_elements(target=rule.json.data.policy_rules[0].index) }}"
File: ./roles/nb_graphql/tasks/main.yml
---
# Tasks file for roles/nb_graphql
- name: Policy from Nautobot
check_mode: false
diff: false
ansible.builtin.uri:
validate_certs: false
url: "https://{{ policy_data }}"
method: POST
follow_redirects: all
headers:
Authorization: "Token {{ api_key }}"
Accept: "application/json; indent=4"
body_format: json
body:
variables:
policyname: "{{ policy_name }}"
delegate_to: localhost
register: rule
- name: To Slack
check_mode: false
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
msg: "{{ policy_name }}"
register: slack_response
delegate_to: localhost
- name: Details to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "Nautobot Object: ```{{ rule.json.data.policy_rules[0] | to_nice_json }}```"
delegate_to: localhost
File: ./roles/nb_graphql/defaults/main.yml
---
# defaults file for roles/nb_graphql
nautobot_host: nautobot.cc.lehigh.edu
# corresponds with the saved query in nautobot
policy_data: "{{ nautobot_host }}/api/extras/graphql-queries/bee72043-be67-4c58-8930-c08f536c9bd5/run/"
api_key: "{{ lookup('file', '/opt/firewall/nbkey') }}"
File: ./roles/nb_graphql/handlers/main.yml
---
# handlers file for roles/nb_graphql
File: ./roles/nb_graphql/vars/main.yml
---
# vars file for roles/nb_graphql
File: ./roles/fortinet_fw/tasks/servicegroup.yml
---
# Task to bundle services together into a service group
- name: Set firewall service group
vars:
members: "{{ item.service_objects | community.general.json_query('[].{name: name}') }}"
fortinet.fortios.fortios_firewall_service_group:
vdom: "{{ vdom }}"
state: "{{ item.status.name | regex_search('Active|Disabled') and 'present' or 'absent' }}"
access_token: "{{ access_token }}"
firewall_service_group:
comment: "{{ item.comment | default('') }}"
name: "{{ item.name }}"
member: "{{ members }}"
with_items: "{{ servicegroup }}"
register: msg
- name: Details to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "ServiceGroup Update: ``` - {{ msg.msg }}\n - Changed: {{ msg.changed }}```"
delegate_to: localhost
File: ./roles/fortinet_fw/tasks/service.yml
---
- name: Set firewall service
vars:
# Hack to lookup the IP protocol number since nb doesn't provide yet
# https://github.com/nautobot/nautobot-plugin-firewall-models/issues/169
ip_protocol_number: "{{ lookup('ansible.builtin.csvfile', item.ip_protocol + ' file=../files/iana_reversed.csv delimiter=,') | default('') }}"
fortinet.fortios.fortios_firewall_service_custom:
vdom: "{{ vdom }}"
state: "{{ item.status.name | regex_search('Active|Disabled') and 'present' or 'absent' }}"
access_token: "{{ access_token }}"
firewall_service_custom:
comment: "{{ item.comment | default('No Comment') }}"
name: "{{ item.name }}"
# Weird syntax for matching incoming text and setting protocol field based on it.
# I don't totally understand how it works to be honest
protocol: "{{ item.ip_protocol | regex_search('TCP|UDP') and 'TCP/UDP/SCTP' or item.ip_protocol | regex_search('ICMP') and 'ICMP' or 'IP' }}"
protocol_number: "{{ ip_protocol_number }}"
tcp_portrange: "{{ item.ip_protocol | replace('TCP', item.port) | default('') }}"
udp_portrange: "{{ item.ip_protocol | replace('UDP', item.port) | default('') }}"
with_items: "{{ services }}"
register: msg
- name: Details to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "Service Update: ``` - {{ msg.msg }}\n - Changed: {{ msg.changed }} ```"
delegate_to: localhost
File: ./roles/fortinet_fw/tasks/main.yml
---
# Ordered list of playbooks to execute.
- name: Create address task
collections:
- fortinet.fortios
connection: httpapi
vars:
addresses:
# Doesn't support looping multiple addresses... yet
- "{{ policy.destination_addresses[0] }}"
- "{{ policy.source_addresses[0] }}"
ansible.builtin.import_tasks: address.yml
- name: Create services
collections:
- fortinet.fortios
connection: httpapi
vars:
# Doeesn't support looping multiple service_groups... yet
services: "{{ policy.destination_service_groups[0].service_objects }}"
ansible.builtin.import_tasks: service.yml
- name: Create service groups
collections:
- fortinet.fortios
connection: httpapi
vars:
servicegroup: "{{ policy.destination_service_groups }}"
ansible.builtin.import_tasks: servicegroup.yml
- name: Create policy
collections:
- fortinet.fortios
connection: httpapi
ansible.builtin.import_tasks: policy.yml
File: ./roles/fortinet_fw/tasks/address.yml
---
# Ansible Playbook to create or delete firewall addresses on a Fortinet device
- name: Address management
fortinet.fortios.fortios_firewall_address:
vdom: "{{ vdom }}"
# If the state of the object is set to anything other than 'Active' or 'Disabled'
# the object will be deleted.
state: "{{ item.status.name | regex_search('Active|Disabled') and 'present' or 'absent' }}"
access_token: "{{ access_token }}"
# Define the firewall address parameters
firewall_address:
# Use the comment field from the item or provide a default if not specified
comment: "{{ item.comment | default('') }}"
name: "{{ item.name }}"
# Set the CIDR for the firewall address using the IP address from the item
subnet: "{{ item.ip_address.address }}"
type: ipmask
# Loop through all of the items in the addresses object.
with_items: "{{ addresses }}"
register: msg
- name: Details to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "Address Update: ``` - {{ msg.msg }}\n - Changed: {{msg.changed}}```"
delegate_to: localhost
File: ./roles/fortinet_fw/tasks/policy.yml
---
# Task to set a new firewall policy rule based on a given data structure
- name: Firewall policy
vars:
# Policy ID is based on the index value set in data
policyid: "{{ policy.index }}"
fortinet.fortios.fortios_firewall_policy:
vdom: "{{ vdom }}"
# If status is either Active or Disabled, state = present, otherwise it is absent
state: "{{ policy.status.name | regex_search('Active|Disabled') and 'present' or 'absent' }}"
access_token: "{{ access_token }}"
firewall_policy:
policyid: "{{ policyid }}"
# Mapping ALLOW to accept and DENY to deny
action: "{{ policy.action | replace('ALLOW', 'accept') | replace('DENY', 'deny') }}"
comments: "{{ policy.comment | default('') }}"
name: "{{ policy.name }}"
# Fun json query to extract the name fields from complex objects
dstaddr: "{{ policy.destination_addresses | community.general.json_query('[].{name: name}') }}"
srcaddr: "{{ policy.source_addresses | community.general.json_query('[].{name: name}') }}"
service: "{{ policy.destination_service_groups | community.general.json_query('[].{name: name}') }}"
dstintf:
- "{{ policy.destination_zone.name | community.general.dict_kv('name') }}"
srcintf:
- "{{ policy.source_zone.name | community.general.dict_kv('name') }}"
schedule: "always"
# Using status field again here to map Disabled to disable
status: "{{ policy.status.name | regex_search('Disabled|Expired') and 'disable' or 'enable' }}"
register: msg
- name: Details to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "Policy Invocation: ```Changed: {{ msg.changed }}```"
delegate_to: localhost
- name: Debug
ansible.builtin.debug:
var: policy.action
- name: Move rule
# Task to move a rule to the top of the list if its action is 'Deny'
# Only runs when action is set to Deny
fortinet.fortios.fortios_firewall_policy:
access_token: "{{ access_token }}"
vdom: "{{ vdom }}"
action: move
self: "{{ policy.index }}"
before: "{{ before }}"
when: policy.action == 'DENY'
register: status
- name: Summary to Slack
community.general.slack:
token: "{{ slack_token }}"
channel: "#netops"
thread_id: "{{ slack_response['ts'] }}"
msg: "Move Details: ``` Changed: {{ status.changed }}```"
delegate_to: localhost
File: ./roles/fortinet_fw/defaults/main.yml
---
# defaults file for roles/fortinet_fw
ansible_network_os: fortinet.fortios.fortios
ansible_httpapi_use_ssl: true
ansible_httpapi_validate_certs: false
ansible_httpapi_port: 443
vdom: FG-traffic
File: ./roles/fortinet_fw/handlers/main.yml
---
# handlers file for roles/fortinet_fw
File: ./roles/fortinet_fw/vars/main.yml
---
# vars file for roles/fortinet_fw
access_token: "{{ lookup('file', policy.policies[0].assigned_devices[0].secrets_group.secrets[0].parameters.path) }}"