diff --git a/.gitignore b/.gitignore index 17d3e4f..8c23f37 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ __pycache__/ # VSCode .vscode + +# Ignore ansible workspace +.ansible diff --git a/roles/sap_firewall/README.md b/roles/sap_firewall/README.md index 7e24f99..a8fc2a7 100644 --- a/roles/sap_firewall/README.md +++ b/roles/sap_firewall/README.md @@ -1,66 +1,235 @@ + # sap_firewall Ansible Role - -Ansible role for updating firewall port entries based on SAP instance numbers - -- **Generic** - use the `generic` option to update the ports directly by providing a list of ports -- **SAP NW** - use the `nw` option to update SAP NW ports -- **SAP HANA** - use the `hana` option to update SAP HANA ports - -## Overview - -![](/docs/diagrams/workflow_role_sap_firewall.svg) - -### Variables - -| **Variable** | **Info** | **Default** | **Required** | -| :--- | :--- | :--- | :--- | -| sap_firewall_type | 'generic' / 'nw' / 'hana' | 'generic' | yes | -| sap_firewall_ports | List of ports to update when using the option 'generic' | | no, only if 'generic' | -| sap_firewall_instance_nr | SAP instance number when using options 'hana' / 'nw' | | no, only if 'nw' / 'hana' | -| sap_firewall_disable_end | Set this parameter to true to disable firewall at the end of execution | | no | - -### Input and Execution - -- Sample execution: - - ```bash - ansible-playbook --connection=local --limit localhost -i "localhost," sap-firewall-update.yml -e "@input_file.yml" - ``` - -- Sample playbook using `generic` option - - ```yaml - --- - - hosts: all - become: true + + +## Description + +The Ansible role `sap_firewall` configures `firewalld` with recommended rules for SAP Systems or custom ports. + + + +## Dependencies +- `ansible.posix` + - Modules: + - `firewalld` + - This collection is part of main Ansible package. + + +## Prerequisites + +This Ansible role installs Python libraries (bindings) for `firewalld` on operating systems where they are not included as a dependency of the `firewalld` package. +For example, the `python311-firewall` package might need to be installed on some OS versions, while on others it is included as a dependency of `firewalld`. + +> **SUSE NOTE for SLES 15 and older**: Firewall bindings packages compatible with Python 3.11 (e.g., `python311-firewall`) are available only in SLES 15 SP6+. +> Existing `python3-firewall` on SLES 15 SP5 or older is not compatible with Python 3.11. + + +## Execution + +> **NOTE: Predefined presets contain recommended ports.** +> You can design your own port openings using `sap_firewall_ports`, if none of presets suit your requirements. + +### Available Presets +Following presets are defined by Ansible Role with ports below. +> `NN` specifies the SAP Instance Number defined in `sap_firewall_instance_number`. + +#### Preset: netweaver +| Ports | Protocol | Reason | +| --- | --- | --- | +| 1128-1129 | TCP | SAP Host Agent ports for status and metrics communication. | +| 3200-3399 | TCP | Essential SAP Gateway (33NN) and SAP Dispatcher (32NN) communication for all instance numbers (00-99) | +| 36NN | TCP | SAP Message Server port (36NN). Used for internal communication between application servers. | +| 80NN | TCP | SAP Internet Communication Manager (ICM) HTTP port (80NN). Used for non-secure web client access. | +| 81NN | TCP | SAP Message Server HTTP port (81NN), configured via the `ms/http_port_` profile parameter. | +| 443NN | TCP | SAP Internet Communication Manager (ICM) HTTPS port (443NN). Used for secure web client access. | +| 5NN13
5NN14
5NN16 | TCP | SAP Start Service (sapstartsrv) communication. Used for service control and status checks. | +| 620NN
621NN | TCP | JAVA ports (62NNN), commonly used for communication within the AS-Java stack, e.g., P4/P4S protocols. | + +#### Preset: hana +| Ports | Protocol | Reason | +| --- | --- | --- | +| 1128-1129 | TCP | SAP Host Agent ports for status and metrics communication. | +| 5050 | TCP | SAP HANA Data Provisioning Agent (DP Agent) port for SDA/SDI connectivity. | +| 43NN | TCP | SAP HANA Web Dispatcher HTTPS port. | +| 3NN90 | TCP | SAP HANA SQL and internal communication range for the IndexServer and distributed nodes. | +| 30105
30107
30140 | TCP | SAP HANA System Replication (SR) and internal endpoint communication. | +| 4NN01
4NN02
4NN06
4NN12
4NN14
4NN40 | TCP | SAP HANA Extended Application Services (XS) and other key internal services. | +| 5NN13
5NN14
5NN16 | TCP | SAP Start Service (sapstartsrv) communication. Used for service control and status checks. | +| 51000-51500 | TCP | SAP HANA XSA runtime port range for the xscontroller-managed Web Dispatcher connection. | +| 64997 | TCP | Internal administration port for the SAP Web Dispatcher, used for local communication only. | + +#### Preset: ha +| Ports | Protocol | Reason | +| --- | --- | --- | +| 5404-5405 | UDP | UDP ports used by Corosync for inter-node communication and cluster heartbeats. | +| 2224 | TCP | TCP port used by the Pacemaker Remote service to manage remote nodes or containerized resources. | + +### Execution Flow + +Execution with `sap_firewall_state` set to `present` (default): +1. Assert and validate all variables. +2. Install `firewalld` and bindings, if required. +3. Add new services based on presets defined in `sap_firewall_presets`. + - Fail if existing service file is detected, unless `sap_firewall_service_force` is set to `true`. +4. Add new ports and services defined in `sap_firewall_ports`. +5. Reload `firewalld` configuration, if required. + +Execution with `sap_firewall_state` set to `absent`: +1. Assert and validate all variables. +2. Install `firewalld` and bindings, if required. + - This is required because `firewall-cmd` commands are executed in online mode. +3. Remove services from `firewall-cmd` based on presets defined in `sap_firewall_presets`. +4. Remove ports and services from `firewall-cmd` defined in `sap_firewall_ports`. +5. Reload `firewalld` configuration, if required. +6. Remove existing service files under `/etc/firewalld/services/` based on presets defined in `sap_firewall_presets`. + + +### Example + +Example of enabling `netweaver` preset and custom TCP port `3700` into `public` zone. +```yaml +--- +- name: Ansible Play for SAP Firewall + hosts: all + become: true + tasks: + - name: Execute Ansible Role sap_firewall + ansible.builtin.include_role: + name: community.sap_operations.sap_firewall + vars: + sap_firewall_state: present + sap_firewall_presets: + - preset: netweaver + zone: public + sap_firewall_instance_number: '90' + sap_firewall_ports: + - zone: public + tcp: + - '3700' +``` + +Example of enabling UDP port range `3700-3701` and existing service `ssh` in `internal` zone. +```yaml +--- +- name: Ansible Play for SAP Firewall + hosts: all + become: true + tasks: + - name: Execute Ansible Role sap_firewall + ansible.builtin.include_role: + name: community.sap_operations.sap_firewall + vars: + sap_firewall_state: present + sap_firewall_ports: + - zone: internal + udp: + - '3700-3701' + service: + - ssh +``` + +Example of removing configured `hana` preset and custom TCP port `3700` into `public` zone. +```yaml +--- +- name: Ansible Play for SAP Firewall + hosts: all + become: true + tasks: + - name: Execute Ansible Role sap_firewall + ansible.builtin.include_role: + name: community.sap_operations.sap_firewall vars: + sap_firewall_state: absent + sap_firewall_presets: + - preset: hana + zone: public + sap_firewall_instance_number: '90' sap_firewall_ports: - - "1128" - - "1129" - sap_firewall_type: "generic" - roles: - - { role: community.sap_operations.sap_firewall } - ``` - -- Sample playbook using `sap_facts` module to get all SAP systems in the host - - ```yaml - --- - - hosts: all - become: true - - tasks: - - - name: Run sap_facts module to gather SAP facts - community.sap_operations.sap_facts: - param: "all" - register: sap_facts_register - - - name: Firewall Update - vars: - sap_firewall_type: "{{ item.Type }}" - sap_firewall_instance_nr: "{{ item.InstanceNumber }}" - ansible.builtin.include_role: - name: community.sap_operations.sap_firewall - loop: "{{ sap_facts_register.sap_facts }}" - ``` + - zone: public + tcp: + - '3700' +``` + +## License + +Apache 2.0 + + +## Maintainers + +- [Marcel Mamula](https://github.com/marcelmamula) + + +## Role Variables + +### sap_firewall_state +- _Type:_ `string` +- _Default:_ `present` + +Sets the desired state of the firewall configuration for SAP. +`present` - Creates and enables the firewall services for SAP. +`absent` - Removes the firewall services for SAP. + +### sap_firewall_presets +- _Type:_ `list` of type `dict` + +A list of SAP Firewall configuration presets to apply. +Each item is a dictionary defining the preset and its zone. +Preset options: +- `hana` - Use predefined ports for SAP HANA. +- `netweaver` - Use predefined ports for SAP NetWeaver. +- `ha` - Use predefined ports for SAP High Availability. +Example zone values: `block, dmz, drop, external, home, internal, public, trusted, work`. +Example: +```yaml +sap_firewall_presets: + - preset: hana + zone: public + - preset: netweaver + zone: internal +``` + +### sap_firewall_ports +- _Type:_ `list` of type `dict` + +A list of custom firewall rules to apply. +Each item in the list is a dictionary that defines a zone and the ports to open. +Example zone values: `block, dmz, drop, external, home, internal, public, trusted, work`. +Example: +```yaml +sap_firewall_ports: + - zone: public + tcp: + - "3200-3399" # A range of ports + - "3600" # A single port + udp: + - "1234" + service: + - "ssh" + - zone: internal + tcp: + - "8080" +``` + +### sap_firewall_instance_number +- _Type:_ `string` + +The SAP Instance number. +Required if sap_firewall_presets contains `hana` or `netweaver`. + +### sap_firewall_end_status +- _Type:_ `string` +- _Default:_ `enabled` + +Status of firewall at the end of the playbook. +This will be used only when `sap_firewall_presets` or `sap_firewall_ports` are not empty. +- `enabled` - Firewall will be enabled and started. +- `disabled` - Firewall will be disabled and stopped. + +### sap_firewall_service_name +- _Type:_ `string` + +The name of the firewall service for SAP. +If not provided, the service name is generated based on `sap_firewall_presets` and `sap_firewall_instance_number`. +Example: `sap-netweaver-00` for `sap_firewall_presets: ['netweaver']` and `sap_firewall_instance_number: '00'`. + diff --git a/roles/sap_firewall/defaults/main.yml b/roles/sap_firewall/defaults/main.yml index 626c78a..7ee666e 100644 --- a/roles/sap_firewall/defaults/main.yml +++ b/roles/sap_firewall/defaults/main.yml @@ -1,12 +1,59 @@ -# Firewall type -sap_firewall_type: "generic" -# generic | hana | nw +# SPDX-License-Identifier: Apache-2.0 +--- -# sap_firewall_type - set to 'generic' will go thru all the ports defined in this list +# Sets the desired state of the firewall configuration for SAP (String). +# 'present' - Creates and enables the firewall services for SAP. +# 'absent' - Removes the firewall services for SAP. +sap_firewall_state: present + +# A list of SAP Firewall configuration presets to apply. +# Each item is a dictionary defining the preset and its zone. +# Preset options: +# hana - Use predefined ports for SAP HANA. +# netweaver - Use predefined ports for SAP NetWeaver. +# ha - Use predefined ports for SAP High Availability. +# Example zone values: block, dmz, drop, external, home, internal, public, trusted, work. +# Example: +# sap_firewall_presets: +# - preset: hana +# zone: public +# - preset: netweaver +# zone: internal +sap_firewall_presets: [] + +# A list of custom firewall rules to apply. +# Each item in the list is a dictionary that defines a zone and the ports to open. +# Example zone values: block, dmz, drop, external, home, internal, public, trusted, work. +# Example: +# sap_firewall_ports: +# - zone: public +# tcp: +# - "3200-3399" # A range of ports +# - "3600" # A single port +# udp: +# - "1234" +# service: +# - "ssh" +# - zone: internal +# tcp: +# - "8080" sap_firewall_ports: [] -# sap_firewall_type - set to 'nw' or 'hana' to make use of instance_nr -sap_firewall_instance_nr: +# The SAP Instance number (String). +# Required if sap_firewall_presets contains 'hana' or 'netweaver'. +sap_firewall_instance_number: '' + +# Status of firewall at the end of the playbook (String). +# This will be used only when 'sap_firewall_presets' or 'sap_firewall_ports' are not empty. +# enabled - Firewall will be enabled and started. +# disabled - Firewall will be disabled and stopped. +sap_firewall_end_status: enabled + +# The name of the firewall service for SAP (String). +# If not provided, the service name is generated based on sap_firewall_presets and sap_firewall_instance_number. +# Example: 'sap-netweaver-00' for sap_firewall_presets: ['netweaver'] and sap_firewall_instance_number: '00'. +# sap_firewall_service_name: '' -# Set this parameter to true to disable firewall at the end of execution -sap_firewall_disable_end: 'false' +# Allow overriding existing configuration files (Boolean). +# If set to true, the role will overwrite an existing firewalld service file if it has the same name as one being created. +# sap_firewall_service_force: false diff --git a/roles/sap_firewall/handlers/main.yml b/roles/sap_firewall/handlers/main.yml new file mode 100644 index 0000000..fb2e795 --- /dev/null +++ b/roles/sap_firewall/handlers/main.yml @@ -0,0 +1,8 @@ +--- +# SPDX-License-Identifier: Apache-2.0 + +# Reload is skipped when firewalld service is set to disabled. +- name: Reload firewalld + ansible.builtin.command: + cmd: firewall-cmd --reload + when: sap_firewall_end_status == 'enabled' | d('enabled') diff --git a/roles/sap_firewall/tasks/assert_ports.yml b/roles/sap_firewall/tasks/assert_ports.yml new file mode 100644 index 0000000..f629932 --- /dev/null +++ b/roles/sap_firewall/tasks/assert_ports.yml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +- name: SAP Firewall - Assert that TCP and UDP ports are defined as String + ansible.builtin.assert: + that: + - port_list_item is string + fail_msg: | + FAIL: Port '{{ port_list_item | string }}' in zone '{{ __sap_firewall_port_assert.zone }}' is not a String. + Data type: {{ port_list_item | type_debug }} + quiet: true + loop: "{{ (__sap_firewall_port_assert.tcp | default([])) + (__sap_firewall_port_assert.udp | default([])) }}" + loop_control: + loop_var: port_list_item + label: "Port: {{ port_list_item }}" + +- name: SAP Firewall - Assert that single TCP and UDP ports are valid + ansible.builtin.assert: + that: + - port_list_item is match('^([1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$') + fail_msg: | + FAIL: Port '{{ port_list_item }}' in zone '{{ __sap_firewall_port_assert.zone }}' is invalid. + Must be a single port between 1 and 65535. + quiet: true + loop: "{{ (__sap_firewall_port_assert.tcp | default([])) + (__sap_firewall_port_assert.udp | default([])) }}" + loop_control: + loop_var: port_list_item + label: "Port: {{ port_list_item }}" + when: "'-' not in port_list_item" + +- name: SAP Firewall - Assert that TCP and UDP port ranges are valid + ansible.builtin.assert: + that: + - port_list_item.split('-') | length == 2 + - port_list_item.split('-')[0] is match('^([1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$') + - port_list_item.split('-')[1] is match('^([1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$') + - port_list_item.split('-')[0] | int <= port_list_item.split('-')[1] | int + fail_msg: | + {% if port_list_item.split('-') | length != 2 %} + FAIL: Port range '{{ port_list_item }}' in zone '{{ __sap_firewall_port_assert.zone }}' is not a valid range. It should contain exactly one '-'. + {% elif port_list_item.split('-')[0] | int > port_list_item.split('-')[1] | int %} + FAIL: Invalid port range '{{ port_list_item }}' in zone '{{ __sap_firewall_port_assert.zone }}'. + The start port cannot be greater than the end port. + {% else %} + FAIL: Invalid port range '{{ port_list_item }}' in zone '{{ __sap_firewall_port_assert.zone }}'. + Both start and end of range must be a valid port number between 1 and 65535. + {% endif %} + quiet: true + loop: "{{ (__sap_firewall_port_assert.tcp | default([])) + (__sap_firewall_port_assert.udp | default([])) }}" + loop_control: + loop_var: port_list_item + label: "Port: {{ port_list_item }}" + when: "'-' in port_list_item" diff --git a/roles/sap_firewall/tasks/assert_variables.yml b/roles/sap_firewall/tasks/assert_variables.yml new file mode 100644 index 0000000..ef4bdb0 --- /dev/null +++ b/roles/sap_firewall/tasks/assert_variables.yml @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# Validate mandatory variables +- name: SAP Firewall - Assert that the variable 'sap_firewall_state' is defined with valid value + ansible.builtin.assert: + that: + - sap_firewall_state is defined + - sap_firewall_state is string + - sap_firewall_state | trim | length > 0 + - sap_firewall_state in ['present', 'absent'] + success_msg: | + PASS: The 'sap_firewall_state' variable is defined with a valid value. + fail_msg: | + {% if sap_firewall_state is not string %} + FAIL: The 'sap_firewall_state' variable must be a string, but it is not. + {% elif sap_firewall_state | trim | length == 0 %} + FAIL: The 'sap_firewall_state' variable cannot be an empty string. + {% else %} + FAIL: The value '{{ sap_firewall_state }}' is not valid for 'sap_firewall_state'. + Available values are 'present' or 'absent'. + {% endif %} + +# This will be used for 'state' parameter in posix.firewall. +- name: SAP Firewall - Set firewall action state + ansible.builtin.set_fact: + __sap_firewall_state: "{{ 'enabled' if sap_firewall_state == 'present' else 'disabled' }}" + + +- name: Block to assert sap_firewall_presets + when: + - sap_firewall_presets is defined + - sap_firewall_presets | length > 0 + block: + - name: SAP Firewall - Assert that the variable 'sap_firewall_presets' is defined as a list + ansible.builtin.assert: + that: + - sap_firewall_presets | type_debug == 'list' + success_msg: | + PASS: The 'sap_firewall_presets' variable is a list. + fail_msg: | + FAIL: The 'sap_firewall_presets' variable must be a list, but it is of type '{{ sap_firewall_presets | type_debug }}'. + + - name: SAP Firewall - Assert that the variable 'sap_firewall_presets' contains valid presets + ansible.builtin.assert: + that: + - preset_item.preset is defined + - preset_item.preset | string in ['hana', 'netweaver', 'ha'] + success_msg: | + PASS: The value '{{ preset_item.preset | d('') }}' is a valid preset. + fail_msg: | + {% if preset_item.preset is not defined %} + FAIL: A list item in 'sap_firewall_presets' is missing the 'preset' key. Item: {{ preset_item }} + {% else %} + FAIL: '{{ preset_item.preset }}' is not a valid value for 'preset'. + Allowed presets are: 'hana', 'netweaver', 'ha'. + {% endif %} + quiet: true + loop: "{{ sap_firewall_presets }}" + loop_control: + loop_var: preset_item + label: "Preset: {{ preset_item.preset | d('UNDEFINED') }}" + + +- name: Block to assert sap_firewall_ports + when: + - sap_firewall_ports is defined + - sap_firewall_ports | length > 0 + block: + - name: SAP Firewall - Assert that the variable 'sap_firewall_ports' is defined as a list + ansible.builtin.assert: + that: + - sap_firewall_ports | type_debug == 'list' + success_msg: | + PASS: The 'sap_firewall_ports' variable is a list. + fail_msg: | + FAIL: The 'sap_firewall_ports' variable must be a list, but it is of type '{{ sap_firewall_ports | type_debug }}'. + + - name: SAP Firewall - Assert that the variable 'sap_firewall_ports' contains valid zones + ansible.builtin.assert: + that: + - port_item.zone is defined + success_msg: | + PASS: The value '{{ port_item.zone | d('') }}' is a valid zone. + fail_msg: | + FAIL: A list item in 'sap_firewall_ports' is missing the required 'zone' key. Item: {{ port_item }} + quiet: true + loop: "{{ sap_firewall_ports }}" + loop_control: + loop_var: port_item + label: "Zone: {{ port_item.zone | d('UNDEFINED') }}" + + - name: SAP Firewall - Assert that the variable 'sap_firewall_ports' contains non empty protocols or services + ansible.builtin.assert: + that: + - port_item.keys() | intersect(['tcp', 'udp', 'service']) | length > 0 + - (port_item.tcp is not defined) or (port_item.tcp | type_debug == 'list' and port_item.tcp | length > 0) + - (port_item.udp is not defined) or (port_item.udp | type_debug == 'list' and port_item.udp | length > 0) + - (port_item.service is not defined) or (port_item.service | type_debug == 'list' and port_item.service | length > 0) + success_msg: | + PASS: Zone '{{ port_item.zone }}' contains at least one non-empty list of ports or services. + fail_msg: | + {% if port_item.keys() | intersect(['tcp', 'udp', 'service']) | length == 0 %} + FAIL: Zone '{{ port_item.zone }}' must contain at least one of 'tcp', 'udp', or 'service'. + {% else %} + FAIL: In zone '{{ port_item.zone }}', any defined 'tcp', 'udp', or 'service' key must be a non-empty list. + {% endif %} + quiet: true + loop: "{{ sap_firewall_ports }}" + loop_control: + loop_var: port_item + label: "Zone: {{ port_item.zone }}" + + - name: SAP Firewall - Assert that TCP and UDP ports are valid + ansible.builtin.include_tasks: + file: assert_ports.yml + loop: "{{ sap_firewall_ports }}" + loop_control: + loop_var: port_item + label: "Zone: {{ port_item.zone }}" + vars: + __sap_firewall_port_assert: "{{ port_item }}" + when: + - (port_item.tcp is defined and port_item.tcp | length > 0) + or (port_item.udp is defined and port_item.udp | length > 0) + + +# Validate variables for 'hana' and 'netweaver' firewall type +- name: SAP Firewall - Assert that the variable 'sap_firewall_instance_number' is defined as String consisting of 2 digits + ansible.builtin.assert: + that: + - sap_firewall_instance_number is defined + - sap_firewall_instance_number is string + - sap_firewall_instance_number | trim | length == 2 + - sap_firewall_instance_number is match('^[0-9]{2}$') + success_msg: | + PASS: The SAP Instance Number '{{ sap_firewall_instance_number }}' is defined as String consisting of 2 digits. + fail_msg: | + {% if sap_firewall_instance_number is not string %} + FAIL: The 'sap_firewall_instance_number' variable must be a string. + {% elif sap_firewall_instance_number | trim | length == 0 %} + FAIL: The 'sap_firewall_instance_number' variable cannot be empty. + {% else %} + FAIL: The SAP Instance Number '{{ sap_firewall_instance_number }}' is not valid. + It must be a two-digit string (e.g., '00', '42'). + {% endif %} + when: + - sap_firewall_presets is defined + - sap_firewall_presets | map(attribute='preset') | intersect(['hana', 'netweaver']) | length > 0 + +- name: SAP Firewall - Assert that the variable 'sap_firewall_service_name' is defined as non empty String + ansible.builtin.assert: + that: + - sap_firewall_service_name is string + - sap_firewall_service_name | trim | length > 0 + success_msg: | + PASS: The 'sap_firewall_service_name' variable is a non-empty string. + fail_msg: | + {% if sap_firewall_service_name is not string %} + FAIL: The 'sap_firewall_service_name' variable must be a string, but it is of type '{{ sap_firewall_service_name | type_debug }}'. + {% else %} + FAIL: The 'sap_firewall_service_name' variable cannot be an empty string. + {% endif %} + when: + - sap_firewall_presets is defined + - sap_firewall_presets | map(attribute='preset') | intersect(['hana', 'netweaver', 'ha']) | length > 0 + - sap_firewall_service_name is defined + + +# Show warnings without immediate failure. +- name: SAP Firewall - Inform that host will be skipped (No Action) + ansible.builtin.debug: + msg: | + WARN: Both the 'sap_firewall_ports' and 'sap_firewall_presets' variables are undefined or empty. + Configuration steps will be skipped for this host. + when: + - (sap_firewall_ports is not defined or sap_firewall_ports | length == 0) + - (sap_firewall_presets is not defined or sap_firewall_presets | length == 0) + +- name: SAP Firewall - Inform that host will be skipped (Docker) + ansible.builtin.debug: + msg: | + WARN: This role is restricted from running inside a Docker container. + Configuration steps will be skipped for this host. + virtualization_type: {{ ansible_facts['virtualization_type'] }} + when: ansible_facts['virtualization_type'] == "docker" diff --git a/roles/sap_firewall/tasks/configure_ports.yml b/roles/sap_firewall/tasks/configure_ports.yml new file mode 100644 index 0000000..20cea92 --- /dev/null +++ b/roles/sap_firewall/tasks/configure_ports.yml @@ -0,0 +1,61 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# Port protocols are separated into own tasks to simplify loop logic and improve readability. + +- name: SAP Firewall - Manage TCP ports in zone {{ __sap_firewall_port.zone }} + ansible.posix.firewalld: + zone: "{{ __sap_firewall_port.zone }}" + port: "{{ port_list_item }}/tcp" + permanent: true + immediate: true + state: "{{ __sap_firewall_state }}" + loop: "{{ __sap_firewall_port.tcp | default([]) }}" + loop_control: + loop_var: port_list_item + label: "Port: {{ port_list_item }}" + when: + - __sap_firewall_port.tcp is defined + - __sap_firewall_port.tcp | length > 0 + - __sap_firewall_port.tcp | type_debug == 'list' + - port_list_item is string + notify: Reload firewalld + +- name: SAP Firewall - Manage UDP ports in zone {{ __sap_firewall_port.zone }} + ansible.posix.firewalld: + zone: "{{ __sap_firewall_port.zone }}" + port: "{{ port_list_item }}/udp" + permanent: true + immediate: true + state: "{{ __sap_firewall_state }}" + loop: "{{ __sap_firewall_port.udp | default([]) }}" + loop_control: + loop_var: port_list_item + label: "Port: {{ port_list_item }}" + when: + - __sap_firewall_port.udp is defined + - __sap_firewall_port.udp | length > 0 + - __sap_firewall_port.udp | type_debug == 'list' + - port_list_item is string + notify: Reload firewalld + + +- name: Block for services + when: + - __sap_firewall_port.service is defined + - __sap_firewall_port.service | length > 0 + - __sap_firewall_port.service | type_debug == 'list' + - service_item is string + block: + - name: SAP Firewall - Manage services in zone {{ __sap_firewall_port.zone }} + ansible.posix.firewalld: + zone: "{{ __sap_firewall_port.zone }}" + service: "{{ service_item }}" + permanent: true + immediate: true + state: "{{ __sap_firewall_state }}" + loop: "{{ __sap_firewall_port.service | default([]) }}" + loop_control: + loop_var: service_item + label: "Service: {{ service_item }}" + notify: Reload firewalld diff --git a/roles/sap_firewall/tasks/configure_preset.yml b/roles/sap_firewall/tasks/configure_preset.yml new file mode 100644 index 0000000..f397f44 --- /dev/null +++ b/roles/sap_firewall/tasks/configure_preset.yml @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# This task file will create temporary service file and compare it with existing one. +# If they differ, and override is not allowed, the task will fail. + +- name: SAP Firewall - Determine service name - {{ __sap_firewall_preset.preset }} + ansible.builtin.set_fact: + __sap_firewall_service_name: >- + {{ + sap_firewall_service_name + if sap_firewall_service_name | default('') | trim | length > 0 + else lookup('ansible.builtin.vars', '__sap_firewall_service_name_' ~ __sap_firewall_preset.preset) + }} + +- name: SAP Firewall - Stat existing service file - {{ __sap_firewall_preset.preset | d('') }} + ansible.builtin.stat: + path: "{{ __sap_firewall_service_path }}/{{ __sap_firewall_service_name }}.xml" + register: __sap_firewall_service_stat_service_existing + + +- name: Block for creating new service file + when: sap_firewall_state == 'present' + block: + + - name: SAP Firewall - Create temporary file for generation - {{ __sap_firewall_preset.preset }} + ansible.builtin.tempfile: + state: file + suffix: _sap_firewall_service + register: __sap_firewall_register_service_tempfile + + - name: SAP Firewall - Generate service file from template - {{ __sap_firewall_preset.preset }} + ansible.builtin.template: + src: sap-{{ __sap_firewall_preset.preset }}.j2 + dest: "{{ __sap_firewall_register_service_tempfile.path }}" + owner: 'root' + group: 'root' + mode: '0640' + + - name: SAP Firewall - Stat new service file - {{ __sap_firewall_preset.preset | d('') }} + ansible.builtin.stat: + path: "{{ __sap_firewall_register_service_tempfile.path }}" + register: __sap_firewall_service_stat_service_new + when: __sap_firewall_service_stat_service_existing.stat.exists + + - name: SAP Firewall - Fail if service file differs to existing - {{ __sap_firewall_preset.preset }} + ansible.builtin.fail: + msg: | + FAIL: Existing service file differs to the newly generated one. + Path: {{ __sap_firewall_service_path }}/{{ __sap_firewall_service_name }}.xml + + The new content differs from the existing content. + To allow this change and overwrite the file, run the playbook again + with the variable 'sap_firewall_service_force' se to true (Boolean). + when: + - not sap_firewall_service_force | default(false) + - __sap_firewall_service_stat_service_existing.stat.exists + - __sap_firewall_service_stat_service_existing.stat.checksum != __sap_firewall_service_stat_service_new.stat.checksum + + + - name: SAP Firewall - Copy new service file to destination - {{ __sap_firewall_preset.preset }} + ansible.builtin.copy: + src: "{{ __sap_firewall_register_service_tempfile.path }}" + dest: "{{ __sap_firewall_service_path }}/{{ __sap_firewall_service_name }}.xml" + owner: 'root' + group: 'root' + mode: '0640' + remote_src: true + register: __sap_firewall_register_copy_service + changed_when: __sap_firewall_register_copy_service.changed + + - name: SAP Firewall - Reload firewalld after applying new service file - {{ __sap_firewall_preset.preset }} + ansible.builtin.command: + cmd: firewall-cmd --reload + when: __sap_firewall_register_copy_service.changed + + - name: SAP Firewall - Remove temporary service file - {{ __sap_firewall_preset.preset }} + ansible.builtin.file: + path: "{{ __sap_firewall_register_service_tempfile.path }}" + state: absent + + +- name: SAP Firewall - Configure service in firewall - {{ __sap_firewall_preset.preset }} + ansible.posix.firewalld: + zone: "{{ __sap_firewall_preset.zone | d('public') }}" + service: "{{ __sap_firewall_service_name }}" + permanent: true + immediate: true + state: "{{ __sap_firewall_state }}" + notify: Reload firewalld + + +- name: SAP Firewall - Cleanup service files for 'absent' - {{ __sap_firewall_preset.preset }} + ansible.builtin.file: + path: "{{ __sap_firewall_service_path }}/{{ __sap_firewall_service_name }}.xml" + state: absent + when: sap_firewall_state == 'absent' + notify: Reload firewalld diff --git a/roles/sap_firewall/tasks/enable_firewall.yml b/roles/sap_firewall/tasks/enable_firewall.yml index be10ea7..4135fec 100644 --- a/roles/sap_firewall/tasks/enable_firewall.yml +++ b/roles/sap_firewall/tasks/enable_firewall.yml @@ -1,6 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 --- -- name: SAP Firewall - Service firewalld state - start & enable - ansible.builtin.systemd: + +- name: SAP Firewall - Install firewalld packages + ansible.builtin.package: + name: "{{ __sap_firewall_packages + __sap_firewall_extra_packages }}" + state: present + +- name: SAP Firewall - Start and enable firewalld service + ansible.builtin.systemd_service: name: firewalld state: started enabled: true diff --git a/roles/sap_firewall/tasks/generate_ports_generic.yml b/roles/sap_firewall/tasks/generate_ports_generic.yml deleted file mode 100644 index 3e2690d..0000000 --- a/roles/sap_firewall/tasks/generate_ports_generic.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: SAP Firewall - Generate Ports - Generic - ansible.builtin.set_fact: - sap_firewall_ports: "{{ sap_firewall_ports }}" - when: - - sap_firewall_ports is defined diff --git a/roles/sap_firewall/tasks/generate_ports_hana.yml b/roles/sap_firewall/tasks/generate_ports_hana.yml deleted file mode 100644 index 5c59423..0000000 --- a/roles/sap_firewall/tasks/generate_ports_hana.yml +++ /dev/null @@ -1,26 +0,0 @@ -- name: SAP Firewall - Generate Ports - SAP HANA - {{ sap_firewall_instance_nr }} - ansible.builtin.set_fact: - sap_firewall_ports: - - "1128" - - "1129" - - "43{{ sap_firewall_instance_nr }}" - - "5050" - - "9090" - - "9091" - - "9092" - - "9093" - - "3{{ sap_firewall_instance_nr }}00-3{{ sap_firewall_instance_nr }}90" - - "30105" - - "30107" - - "30140" - - "4{{ sap_firewall_instance_nr }}01" - - "4{{ sap_firewall_instance_nr }}02" - - "4{{ sap_firewall_instance_nr }}06" - - "4{{ sap_firewall_instance_nr }}12" - - "4{{ sap_firewall_instance_nr }}14" - - "4{{ sap_firewall_instance_nr }}40" - - "5{{ sap_firewall_instance_nr }}00" - - "5{{ sap_firewall_instance_nr }}13" - - "5{{ sap_firewall_instance_nr }}14" - - "51000" - - "64997" diff --git a/roles/sap_firewall/tasks/generate_ports_nw.yml b/roles/sap_firewall/tasks/generate_ports_nw.yml deleted file mode 100644 index da9b953..0000000 --- a/roles/sap_firewall/tasks/generate_ports_nw.yml +++ /dev/null @@ -1,15 +0,0 @@ -- name: SAP Firewall - Generate Ports - SAP NW - {{ sap_firewall_instance_nr }} - ansible.builtin.set_fact: - sap_firewall_ports: - - "3200-3399" - - "36{{ sap_firewall_instance_nr }}" - - "80{{ sap_firewall_instance_nr }}" - - "81{{ sap_firewall_instance_nr }}" - - "443{{ sap_firewall_instance_nr }}" - - "620{{ sap_firewall_instance_nr }}" - - "621{{ sap_firewall_instance_nr }}" - - "5{{ sap_firewall_instance_nr }}13" - - "5{{ sap_firewall_instance_nr }}14" - - "5{{ sap_firewall_instance_nr }}16" - - "44390" - - "8090" diff --git a/roles/sap_firewall/tasks/main.yml b/roles/sap_firewall/tasks/main.yml index 6e567c5..fac3f6e 100644 --- a/roles/sap_firewall/tasks/main.yml +++ b/roles/sap_firewall/tasks/main.yml @@ -1,34 +1,87 @@ +# SPDX-License-Identifier: Apache-2.0 --- -- name: SAP Firewall - block: - - name: SAP Firewall - Gathering Package Facts - ansible.builtin.package_facts: - manager: auto - - name: SAP Firewall - Setup - block: - - name: SAP Firewall - Enable Firewall - ansible.builtin.include_tasks: "enable_firewall.yml" +# Example of files loading order: +# 1. Suse.yml / RedHat.yml - Specific to OS family. +# 2. SLES_15.yml / RedHat_9.yml - Specific to distribution (SLES, SLES_SAP or RedHat) and major release. +# 3. SLES_15.6.yml / RedHat_9.2 - Specific to distribution (SLES, SLES_SAP or RedHat) and minor release. +# 4. SLES_SAP_15.yml - Specific to distribution SLES_SAP and major release. +# 5. SLES_SAP_15.6.yml - Specific to distribution SLES_SAP and minor release. +- name: SAP Firewall - Include OS specific vars + ansible.builtin.include_vars: "{{ __vars_file }}" + loop: "{{ __var_files }}" + vars: + __vars_file: "{{ role_path }}/vars/{{ item }}" + __distribution_major: "{{ ansible_facts['distribution'] ~ '_' ~ ansible_facts['distribution_major_version'] }}" + __distribution_minor: "{{ ansible_facts['distribution'] ~ '_' ~ ansible_facts['distribution_version'] }}" + # Enables loading of shared vars between SLES and SLES_SAP + __distribution_major_split: "{{ ansible_facts['distribution'].split('_')[0] ~ '_' ~ ansible_facts['distribution_major_version'] }}" + __distribution_minor_split: "{{ ansible_facts['distribution'].split('_')[0] ~ '_' ~ ansible_facts['distribution_version'] }}" + __var_files: >- + {{ + [ + ansible_facts['os_family'] ~ '.yml', + (ansible_facts['distribution'] ~ '.yml') if ansible_facts['distribution'] != ansible_facts['os_family'] else None, + (__distribution_major_split ~ '.yml') if __distribution_major_split != __distribution_major else None, + (__distribution_minor_split ~ '.yml') if __distribution_minor_split != __distribution_minor else None, + __distribution_major ~ '.yml', + __distribution_minor ~ '.yml' + ] | select('defined') | select('string') | list + }} + when: __vars_file is file + + +- name: SAP Firewall - Prepare and validate variables + ansible.builtin.include_tasks: + file: assert_variables.yml - - name: SAP Firewall - Generate Ports - ansible.builtin.include_tasks: "generate_ports_{{ sap_firewall_type }}.yml" - - name: SAP Firewall - Add ports - block: - - name: SAP Firewall - Add Ports - ansible.builtin.include_tasks: update_firewall.yml - loop: "{{ sap_firewall_ports }}" - loop_control: - loop_var: passed_port +# This block stops execution on docker containers or when inputs are undefined or empty. +- name: Block for main configuration + when: + - ansible_facts['virtualization_type'] != "docker" + - (sap_firewall_ports is defined and sap_firewall_ports | length > 0) + or (sap_firewall_presets is defined and sap_firewall_presets | length > 0) + block: + + - name: SAP Firewall - Ensure firewalld is present and enabled + ansible.builtin.include_tasks: + file: enable_firewall.yml - when: - - sap_firewall_ports is defined + # We need to check if zones and services is valid and fail fast if not. + - name: SAP Firewall - Validate inputs against available firewalld options + ansible.builtin.include_tasks: + file: validate_inputs.yml - - name: SAP Firewall - Reload Firewall - ansible.builtin.shell: | - firewall-cmd --reload + - name: SAP Firewall - Manage service file for preset + ansible.builtin.include_tasks: + file: configure_preset.yml + loop: "{{ sap_firewall_presets }}" + loop_control: + loop_var: preset_item + vars: + __sap_firewall_preset: "{{ preset_item }}" + when: + - preset_item is defined + - preset_item | length > 0 + - name: SAP Firewall - Manage custom ports from 'sap_firewall_ports' + ansible.builtin.include_tasks: + file: configure_ports.yml + loop: "{{ sap_firewall_ports }}" + loop_control: + loop_var: port_item + label: "Zone: {{ port_item.zone }}" + vars: + __sap_firewall_port: "{{ port_item }}" when: - - '"firewalld" in ansible_facts.packages' + - sap_firewall_ports is defined + - sap_firewall_ports | length > 0 - when: ansible_virtualization_role != "guest" or ansible_virtualization_type != "docker" + - name: SAP Firewall - Stop and disable firewalld service + ansible.builtin.systemd_service: + name: firewalld + state: stopped + enabled: false + masked: false + when: sap_firewall_end_status == 'disabled' | d('enabled') diff --git a/roles/sap_firewall/tasks/update_firewall.yml b/roles/sap_firewall/tasks/update_firewall.yml deleted file mode 100644 index 00de0c3..0000000 --- a/roles/sap_firewall/tasks/update_firewall.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# This task requires the variable passed_port - -- name: SAP Firewall - Update Firewall - {{ passed_port }}/tcp - ansible.posix.firewalld: - zone: public - port: "{{ passed_port }}/tcp" - permanent: true - immediate: true - state: enabled diff --git a/roles/sap_firewall/tasks/validate_inputs.yml b/roles/sap_firewall/tasks/validate_inputs.yml new file mode 100644 index 0000000..88d80b4 --- /dev/null +++ b/roles/sap_firewall/tasks/validate_inputs.yml @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: Apache-2.0 +--- +# Gather firewalld zones and services for validation +- name: SAP Firewall - Get list of configured zones from firewalld + ansible.builtin.command: + cmd: firewall-cmd --get-zones + register: __sap_firewall_register_zones + changed_when: false + +- name: SAP Firewall - Get list of configured services from firewalld + ansible.builtin.command: + cmd: firewall-cmd --get-services + register: __sap_firewall_register_services + changed_when: false + + +# Validate zone for presets +- name: SAP Firewall - Assert that the Preset Zone is available + ansible.builtin.assert: + that: + - preset_item.zone | string in __sap_firewall_register_zones.stdout + success_msg: | + PASS: The zone '{{ preset_item.zone }}' is present in firewalld. + fail_msg: | + FAIL: The zone '{{ preset_item.zone }}' is not preset in firewalld. + Available zones: {{ __sap_firewall_register_zones.stdout }} + quiet: true + loop: "{{ sap_firewall_presets }}" + loop_control: + loop_var: preset_item + label: "Zone: {{ preset_item.zone }}" + when: + - preset_item.zone is defined + - preset_item.zone | length > 0 + - preset_item.zone != 'public' + + +# Validate zone for ports +- name: SAP Firewall - Assert that the Ports Zone is available + ansible.builtin.assert: + that: + - port_item.zone | string in __sap_firewall_register_zones.stdout + success_msg: | + PASS: The zone '{{ port_item.zone }}' is present in firewalld. + fail_msg: | + FAIL: The zone '{{ port_item.zone }}' is not preset in firewalld. + Available zones: {{ __sap_firewall_register_zones.stdout }} + quiet: true + loop: "{{ sap_firewall_ports }}" + loop_control: + loop_var: port_item + label: "Zone: {{ port_item.zone }}" + when: + - sap_firewall_ports is defined + - sap_firewall_ports | length > 0 + + +# Validate service for ports +- name: Block for services + when: + - sap_firewall_ports is defined + - sap_firewall_ports | length > 0 + block: + # Flatten list of all services into one list for assert + - name: SAP Firewall - Get list of all services defined in 'sap_firewall_ports' + ansible.builtin.set_fact: + __sap_firewall_fact_port_services: >- + {{ + sap_firewall_ports + | selectattr('service', 'defined') + | map(attribute='service') + | flatten + }} + + - name: SAP Firewall - Assert that the service is present in firewalld + ansible.builtin.assert: + that: + - service_item | string in __sap_firewall_register_services.stdout + success_msg: | + PASS: The service '{{ service_item }}' is present in firewalld. + fail_msg: | + FAIL: The service '{{ service_item }}' is not preset in firewalld. + Available services: {{ __sap_firewall_register_services.stdout }} + quiet: true + loop: "{{ __sap_firewall_fact_port_services }}" + loop_control: + loop_var: service_item + label: "Service: {{ service_item }}" diff --git a/roles/sap_firewall/templates/sap-ha.j2 b/roles/sap_firewall/templates/sap-ha.j2 new file mode 100644 index 0000000..2c3f933 --- /dev/null +++ b/roles/sap_firewall/templates/sap-ha.j2 @@ -0,0 +1,12 @@ + + + SAP HA Cluster (Pacemaker/Corosync) + Core ports required for High Availability (HA). Maintained by Ansible Role sap_operations.sap_firewall. + + {# Default UDP ports used by Corosync for inter-node communication and cluster heartbeats. #} + + + {# TCP port used by the Pacemaker Remote service to manage remote nodes or containerized resources. #} + + + \ No newline at end of file diff --git a/roles/sap_firewall/templates/sap-hana.j2 b/roles/sap_firewall/templates/sap-hana.j2 new file mode 100644 index 0000000..9ed55a3 --- /dev/null +++ b/roles/sap_firewall/templates/sap-hana.j2 @@ -0,0 +1,42 @@ + + + SAP HANA Database + Ports required for the SAP HANA Database. Maintained by Ansible Role sap_operations.sap_firewall. + + {# SAP Host Agent ports for status and metrics communication. -#} + + + {# SAP HANA Data Provisioning Agent (DP Agent) port for SDA/SDI connectivity. -#} + + + {# SAP HANA Web Dispatcher HTTPS port. -#} + + + {# Main SAP HANA SQL and internal communication range for the IndexServer and distributed nodes. -#} + + + {# Fixed ports for SAP HANA System Replication (SR) and internal endpoint communication. -#} + + + + + {# The 4NNXX range supports SAP HANA Extended Application Services (XS) and other key internal services. -#} + + + + + + + + {# Ports for SAP Service Layer or SAP Start Service for AS-Java/B1 components. -#} + + + + + {# SAP HANA XSA runtime port range for the xscontroller-managed Web Dispatcher connection. -#} + + + {# Internal administration port for the SAP Web Dispatcher, used for local communication only. -#} + + + \ No newline at end of file diff --git a/roles/sap_firewall/templates/sap-netweaver.j2 b/roles/sap_firewall/templates/sap-netweaver.j2 new file mode 100644 index 0000000..78e077f --- /dev/null +++ b/roles/sap_firewall/templates/sap-netweaver.j2 @@ -0,0 +1,33 @@ + + + SAP NetWeaver Application + Ports required for SAP NetWeaver Application Server. Maintained by Ansible Role sap_operations.sap_firewall. + + {# SAP Host Agent ports for status and metrics communication. -#} + + + {# The 3200-3399 range covers all essential SAP Gateway (33NN) and SAP Dispatcher (32NN) communication for all instance numbers (00-99). -#} + + + {# SAP Message Server port (36NN). Used for internal communication between application servers. -#} + + + {# SAP Internet Communication Manager (ICM) HTTP port (80NN). Used for non-secure web client access. -#} + + + {# SAP Message Server HTTP port (81NN), configured via ms/http_port_ profile parameter. -#} + + + {# SAP Internet Communication Manager (ICM) HTTPS port (443NN). Used for secure web client access. -#} + + + {# Ports for SAP Start Service (sapstartsrv) communication. Used for service control and status checks. -#} + + + + + {# JAVA ports (62NNN), commonly used for communication within the AS-Java stack, e.g., P4/P4S protocols. -#} + + + + \ No newline at end of file diff --git a/roles/sap_firewall/vars/SLES_15.yml b/roles/sap_firewall/vars/SLES_15.yml new file mode 100644 index 0000000..afbac02 --- /dev/null +++ b/roles/sap_firewall/vars/SLES_15.yml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +--- +# Variables specific to following versions: +# - SUSE Linux Enterprise Server 15 + +# NOTE: SLES 16 contains dependency between firewalld and python313-firewall package. + +__sap_firewall_packages: + - firewalld + +__sap_firewall_extra_packages: + "{{ __sap_firewall_extra_packages_3 + if ansible_facts['distribution_version'].split('.')[1] | int < 6 + else __sap_firewall_extra_packages_311 }}" + +# The lists of bindings for specific python version +__sap_firewall_extra_packages_3: + - "python3-firewall" + +__sap_firewall_extra_packages_311: + - "python311-firewall" diff --git a/roles/sap_firewall/vars/main.yml b/roles/sap_firewall/vars/main.yml new file mode 100644 index 0000000..e391015 --- /dev/null +++ b/roles/sap_firewall/vars/main.yml @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# SAP ports are defined directly in jinja2 templates located in the 'templates' directory. + +# List of required packages. +__sap_firewall_packages: + - firewalld + +# List of additional packages like python bindings. +__sap_firewall_extra_packages: [] + +# Predefined SAP Firewall service names based on type and instance number. +__sap_firewall_service_name_hana: "sap-hana-{{ sap_firewall_instance_number }}" +__sap_firewall_service_name_netweaver: "sap-netweaver-{{ sap_firewall_instance_number }}" +__sap_firewall_service_name_ha: "sap-ha-{{ sap_firewall_instance_number }}" + +# Path to the firewall service files. +__sap_firewall_service_path: "/etc/firewalld/services"