# Copyright 2022 OCF Ltd. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # -*- coding: utf-8 -*- # vim: ft=yaml --- ######## inherit custom variables from inventory/host_vars/firewalld.yml - name: merge custom vars block: - name: set role variable sources set_fact: role_info: role_defaults_file: "{{ role_path }}/defaults/main.yml" role_override_file: "{{ ansible_inventory_sources[0] | dirname }}/group_vars/{{ role_name }}.yml" vars_return: "placeholder" - set_fact: source_role: "{{ role_name }}" - name: run merge_vars role include_role: name: "merge_vars" vars: a_config_file: "{{ role_info['role_defaults_file'] }}" b_config_file: "{{ role_info['role_override_file'] }}" calling_role: "{{ source_role }}" - name: merge custom vars to vars[] set_fact: { "{{ entry }}": "{{ role_info['vars_return'][entry] }}" } loop: "{{ role_info['vars_return'] | list }}" loop_control: loop_var: entry when: - not role_info['vars_return'] == 'placeholder' delegate_to: localhost ######## setup packages - name: update package facts ansible.builtin.package_facts: manager: auto strategy: all when: ansible_facts['packages'] is not defined - name: install firewalld packages block: - name: install firewalld ansible.builtin.package: name: - firewalld - ipset - nftables state: latest - name: Install python-firewall package: name: python-firewall state: present when: - ansible_facts['os_family'] == 'RedHat' and ansible_facts['distribution_major_version'] == '7' - name: Install python3-firewall package: name: python3-firewall state: present when: - ansible_facts['os_family'] == 'RedHat' and ansible_facts['distribution_major_version'] == '8' when: - vars['firewalld']['enable'] | bool - ansible_facts['packages']['firewalld'] is not defined or ansible_facts['packages']['ipset'] is not defined or ansible_facts['packages']['nftables'] is not defined - name: update service facts ansible.builtin.service_facts: ######## disable firewall - name: disable firewalld ansible.builtin.systemd: name: firewalld enabled: no state: stopped when: - ansible_facts['services']['firewalld.service'] is not defined - not vars['firewalld']['enable'] | bool ######## render firewalld config file - name: update INI entries in firewalld config ini_file: path: "{{ firewalld['firewalld_conf_file'] }}" no_extra_spaces: true # write to root of document not under a section section: null option: "{{ entry.key }}" value: "{{ entry.value }}" loop: "{{ firewalld['firewalld_conf'] | dict2items }}" loop_control: loop_var: entry notify: reload_firewalld when: - firewalld['enable'] | bool ######## map services to zones and networks # map host 'xcat_groups' (hostvars[ansible_hostname]) to services 'xcat_groups' (vars['firewalld']['firewalld_services'] list item ['xcat_groups']) # determine if the service (firewall rule) is applicable to the host - name: map services to zones block: - name: find firewalld services to be applied to each xcat_groups that this host is a member of set_fact: target_services: "{{ target_services | default([]) + [service] }}" when: xcat_group in hostvars[ansible_hostname]['xcat_groups'] with_subelements: - "{{ firewalld['firewalld_services'] }}" - xcat_groups - skip_missing: True loop_control: loop_var: entry vars: xcat_group: "{{ entry.1 }}" service: "{{ entry.0 }}" # - debug: # msg: # - "{{ target_services }}" - name: remove duplicate service entries where host in multiple xcat_groups set_fact: target_services: "{{ target_services | unique }}" when: - firewalld['enable'] | bool ######## configure ipsets - name: configure ipsets block: - name: list existing ipsets in /etc/firewalld/ipsets find: paths: "/etc/firewalld/ipsets/" patterns: "*.xml" recurse: no file_type: file register: ipsets_files_all - name: exclude ipsets managed by ansible set_fact: ipsets_files: "{{ ipsets_files | default([]) + [file_path] }}" loop: "{{ ipsets_files_all['files'] }}" loop_control: loop_var: entry vars: file_path: "{{ entry['path'] }}" file_name: "{{ entry['path'].split('/')[-1].split('.')[0] }}" when: - ipsets_files_all['files'] | length >0 - file_name not in firewalld['firewalld_ipsets'] - file_name not in vars['steel']['xcat_networks'] | list - name: disable ipsets not managed by ansible copy: remote_src: yes src: "{{ file_path }}" dest: "{{ new_file_path }}" loop: "{{ ipsets_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" new_file_path: "{{ entry.split('.')[0] }}.ansible_disabled" register: ipsets_disabled notify: reload_firewalld when: - ipsets_files is defined - ipsets_files | length >0 - file: path: "{{ file_path }}" state: absent loop: "{{ ipsets_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" when: - not ipsets_disabled['skipped'] | bool - name: generate ipsets from steel['xcat_networks'] set_fact: generated_ipsets: "{{ generated_ipsets | default({}) | combine({ 'firewalld_ipsets': { network_name: { 'short': network_name, 'description': description, 'type': 'hash:ip', 'targets': [network_cidr] } } }, recursive=True) }}" # generated_ipsets: "{{ generated_ipsets | default({}) | combine({ 'firewalld_ipsets': { network_name: { 'short': network_name, 'description': description, 'type': 'hash:ip', 'options': { 'maclem': [65536], 'timeout': [300], 'hashsize': [1024] }, 'targets': [network_cidr] } } }, recursive=True) }}" # example with additional options loop: "{{ steel['xcat_networks'] | dict2items }}" loop_control: loop_var: entry vars: network_name: "{{ entry.key }}" network_range: "{{ entry.value['network'] }}" network_mask: "{{ entry.value['netmask'] }}" network_cidr: "{{ network_range }}/{{ (network_range + '/' + network_mask) | ansible.utils.ipaddr('prefix') }}" description: "{{ network_name }} ipset" # required where we have provided custom ipsets - name: merge generated generate ipsets set_fact: firewalld: "{{ firewalld | default({}) | combine( generated_ipsets, recursive=True) }}" when: - generated_ipsets is defined - name: render firewalld ipsets template: src: "{{ role_path }}/templates/ipset_template.xml.j2" dest: /etc/firewalld/ipsets/{{ entry }}.xml loop: "{{ firewalld['firewalld_ipsets'] | list }}" loop_control: loop_var: entry vars: short: "{{ firewalld['firewalld_ipsets'][entry]['short'] }}" description: "{{ firewalld['firewalld_ipsets'][entry]['description'] }}" type: "{{ firewalld['firewalld_ipsets'][entry]['type'] }}" options: "{{ firewalld['firewalld_ipsets'][entry]['options'] }}" targets: "{{ firewalld['firewalld_ipsets'][entry]['targets'] }}" notify: reload_firewalld when: - firewalld['firewalld_ipsets'] is defined when: - firewalld['enable'] | bool ######## configure services - name: configure services block: - name: list existing services in /etc/firewalld/services find: paths: "/etc/firewalld/services/" patterns: "*.xml" recurse: no file_type: file register: services_files_all - name: exclude services managed by ansible set_fact: services_files: "{{ services_files | default([]) + [file_path] }}" loop: "{{ services_files_all['files'] }}" loop_control: loop_var: entry vars: file_path: "{{ entry['path'] }}" file_name: "{{ entry['path'].split('/')[-1].split('.')[0] }}" when: - services_files_all['files'] | length >0 - file_name not in firewalld['firewalld_services'] | map(attribute='name') # - debug: # msg: # - "{{ services_files }}" - name: disable services not managed by ansible copy: remote_src: yes src: "{{ file_path }}" dest: "{{ new_file_path }}" loop: "{{ services_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" new_file_path: "{{ entry.split('.')[0] }}.ansible_disabled" register: services_disabled notify: reload_firewalld when: - services_files is defined - services_files | length >0 # - debug: # msg: # - "{{ services_disabled }}" - file: path: "{{ file_path }}" state: absent loop: "{{ services_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" when: - not services_disabled['skipped'] | bool - name: render firewalld services template: src: "{{ role_path }}/templates/service_template.xml.j2" dest: /etc/firewalld/services/{{ name }}.xml loop: "{{ target_services }}" loop_control: loop_var: entry vars: name: "{{ entry['name'] }}" short: "{{ entry['short'] }}" description: "{{ entry['description'] }}" port: "{{ entry['port'] }}" notify: reload_firewalld when: - firewalld['firewalld_services'] is defined - firewalld['firewalld_services'] | length >0 when: - firewalld['enable'] | bool ######## configure zones - name: configure zones block: # there are no preset zone names, zones are dynamically generated from top level source inventory/networks.yml # to create a custom zone # - a custom firewalld_services entry with an (arbritrary) xcat_networks list item will generate a new zone # - a custom firewalld_ipsets entry named the same as the custom services entry will be required to control ingress # # - name: generate all zone names from xcat_networks entry in 'firewalld_merged['firewalld_services']' - name: generate all zone names from xcat_networks entry in 'firewalld['firewalld_services']' set_fact: zone_list: "{{ zone_list | default([]) + zone }}" loop: "{{ target_services }}" loop_control: loop_var: entry vars: zone: "{{ entry['xcat_networks'] }}" - name: filter on unique zones from services set_fact: zone_list: "{{ zone_list | unique }}" # this is the pivotal task in the playbook to ensure the zones dictionary is in the format accepted for the jinja2 loops in the zone_template.xml.j2 # loop unique zones, match all services bound to the zone using xcat_networks, get a list of service names and format into a list of dicts each with the same key 'name:', render zones template in compatible format for jinja # - name: create zones dictionary set_fact: firewalld_zones: "{{ firewalld_zones | default([]) + ([{ 'name': zone_name, 'short': zone_name, 'description': zone_description, 'source': [{ 'ipset': zone_name }], 'service': service_trim }] ) }}" # firewalld_zones: "{{ firewalld_zones | default([]) + ([{ 'name': zone_name, 'short': zone_name, 'description': zone_description, 'source': [{ 'ipset': zone_name }], 'service': [{ 'name': 'ssh' }, { 'name': 'ftp' }] }] ) }}" # format required loop: "{{ zone_list }}" loop_control: loop_var: entry vars: zone_name: "{{ entry }}" zone_description: "{{ entry }} zone" # use mapping to return list of services service: "{{ target_services | selectattr('xcat_networks', 'search', entry) | map(attribute='name') }}" # # inline jinja to create a list of dicts for the services used in this zone service_format: >- {% set results = [] %} {% for svc in service|default([]) %} {% set sub_results = {} %} {% set _ = sub_results.update({"name": svc}) %} {% set _ = results.append(sub_results) %} {% endfor -%} {{results}} # trim whitespaces to allow ansible to interperet as list item in the firewalld_zones dict service_trim: "{{ service_format | trim }}" # - name: add pre-defined zones from firewalld_merged['firewalld_zones'] - name: add pre-defined zones from firewalld['firewalld_zones'] set_fact: firewalld_zones: "{{ firewalld_zones | default([]) + [entry] }}" loop: "{{ firewalld['firewalld_zones'] }}" loop_control: loop_var: entry when: - firewalld['firewalld_zones'] is defined - firewalld['firewalld_zones'] | length >0 # - debug: # msg: # - "{{ firewalld_zones }}" - name: list existing zones in /etc/firewalld/zones find: paths: "/etc/firewalld/zones/" patterns: "*.xml" recurse: no file_type: file register: zones_files_all - name: exclude zones managed by ansible set_fact: zone_files: "{{ zone_files | default([]) + [file_path] }}" loop: "{{ zones_files_all['files'] }}" loop_control: loop_var: entry vars: file_path: "{{ entry['path'] }}" file_name: "{{ entry['path'].split('/')[-1].split('.')[0] }}" when: - zones_files_all['files'] | length >0 - file_name not in firewalld_zones | map(attribute='name') # - debug: # msg: # - "{{ zone_files }}" - name: disable zones not managed by ansible copy: remote_src: yes src: "{{ file_path }}" dest: "{{ new_file_path }}" loop: "{{ zone_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" new_file_path: "{{ entry.split('.')[0] }}.ansible_disabled" register: zones_disabled notify: reload_firewalld when: - zone_files is defined - zone_files | length >0 # - debug: # msg: # - "{{ zones_disabled }}" - file: path: "{{ file_path }}" state: absent loop: "{{ zone_files }}" loop_control: loop_var: entry vars: file_path: "{{ entry }}" when: - not zones_disabled['skipped'] | bool - name: render firewalld zones template: src: "{{ role_path }}/templates/zone_template.xml.j2" dest: /etc/firewalld/zones/{{ name }}.xml loop: "{{ firewalld_zones | list }}" loop_control: loop_var: entry vars: name: "{{ entry['name'] }}" short: "{{ entry['short'] }}" description: "{{ entry['description'] }}" service: "{{ entry['service'] }}" ipset: "{{ entry['name'] }}" notify: reload_firewalld when: - firewalld_zones is defined - firewalld_zones | length >0 when: - firewalld['enable'] | bool ######## start firewalld # # handler starts/reloads/enables firewalld service # - name: Flush handlers # meta: flush_handlers # - name: Start and enable firewalld # ansible.builtin.systemd: # name: firewalld.service # state: restarted # # daemon_reload: yes # enabled: yes # when: # - ansible_facts['services']['firewalld.service'] is defined # - firewalld['enable'] | bool