diff --git a/roles/sap_profile_update/README.md b/roles/sap_profile_update/README.md index df7aef4..d627ff7 100644 --- a/roles/sap_profile_update/README.md +++ b/roles/sap_profile_update/README.md @@ -1,80 +1,173 @@ + # sap_profile_update Ansible Role - -Ansible role for updating SAP profiles - -- **DEFAULT** profile -- **Instance** profile - -## Overview - -### Variables - -| **Variable** | **Info** | **Default** | **Required** | -| :--- | :--- | :--- | :--- | -| sap_update_profile_sid | SAP system SID | | yes | -| sap_update_profile_instance_nr | SAP system instance number | | yes | -| sap_update_profile_default_profile_params | List of parameters to update in the default profile | | no | -| sap_update_profile_instance_profile_params | List of parameters to update in the instance profile | | no | - -### Input and Execution - -- Sample execution: - - ```bash - ansible-playbook --connection=local --limit localhost -i "localhost," sap-profile-update.yml -e "@input_file.yml" - ``` - -- Sample direct input - - ```yaml - sap_update_profile_sid: "S20" - sap_update_profile_instance_nr: "00" - sap_update_profile_default_profile_params: - - sapgui/user_scripting = TRUE - - ssl/ciphersuites = 135:PFS:HIGH::EC_P256:EC_HIGH - - ssl/client_ciphersuites = 150:PFS:HIGH::EC_P256:EC_HIGH - sap_update_profile_instance_profile_params: - - PHYS_MEMSIZE = 32768 - - icm/server_port_0 = PROT=HTTP,PORT=80$$,PROCTIMEOUT=600,TIMEOUT=3600 - - icm/server_port_1 = PROT=HTTPS,PORT=443$$,PROCTIMEOUT=600,TIMEOUT=3600 - - icm/server_port_2 = PROT=SMTP,PORT=25$$,PROCTIMEOUT=120,TIMEOUT=120 - ``` - -- Sample playbook using `sap_facts` module to get all SAP systems in the host - - ```yaml - --- - - hosts: all - become: true - + + +## Description + +The Ansible role `sap_profile_update` manages parameters in SAP profile files. + + + + + + + + +## Execution + + + +### Execution Flow + +1. Assert all variables. +2. Validate all variables against target host and its profiles. +3. Manage parameters in profiles. +4. Add audit comment in profile file if change occurred. + +#### Examples of audit comments +```bassh +# 2025-12-09 09:58:34 'rdisp/wp_no_btc' added with value '6' by Ansible Role community.sap_operations.sap_profile_update. +# 2025-12-09 10:00:25 'rdisp/wp_no_btc' changed from '6' to '7' by Ansible Role community.sap_operations.sap_profile_update. +# 2025-12-09 10:01:04 'rdisp/wp_no_btc' commented out by Ansible Role community.sap_operations.sap_profile_update. +``` + + + +### Example + +Example of changing the parameter `rdisp/wp_no_btc` for one System `B01`. +```yaml +- name: Ansible Play for SAP Profile update + hosts: host + become: true + tasks: + - name: Execute Ansible Role sap_profile_update + ansible.builtin.include_role: + name: community.sap_operations.sap_profile_update vars: - - sap_update_profile_default_profile_params: - - sapgui/user_scripting = TRUE - - ssl/ciphersuites = 135:PFS:HIGH::EC_P256:EC_HIGH - - ssl/client_ciphersuites = 150:PFS:HIGH::EC_P256:EC_HIGH - sap_update_profile_instance_profile_params: - - PHYS_MEMSIZE = 32768 - - icm/server_port_0 = PROT=HTTP,PORT=80$$,PROCTIMEOUT=600,TIMEOUT=3600 - - icm/server_port_1 = PROT=HTTPS,PORT=443$$,PROCTIMEOUT=600,TIMEOUT=3600 - - icm/server_port_2 = PROT=SMTP,PORT=25$$,PROCTIMEOUT=120,TIMEOUT=120 - - tasks: - - # Gather SAP facts of the host - - name: Run sap_facts module to gather SAP facts - community.sap_operations.sap_facts: - param: "all" - register: sap_facts_register - - # Update all the profiles of the SAP systems in the host - - name: Update all the profiles of the SAP systems in the host - vars: - sap_update_profile_sid: "{{ item.SID }}" - sap_update_profile_instance_nr: "{{ item.InstanceNumber }}" - ansible.builtin.include_role: - name: community.sap_operations.sap_profile_update - loop: "{{ sap_facts_register.sap_facts }}" - when: - - item.Type == 'nw' - ``` + sap_profile_update_definitions: + - sid: 'B01' + profiles: + - type: 'instance' + instance_number: '01' + parameters: + - name: 'rdisp/wp_no_btc' + value: '10' +``` + +Example of resizing instances for multiple Systems on one host with custom path to profiles. +```yaml +- name: Ansible Play for SAP Profile update + hosts: host + become: true + tasks: + - name: Execute Ansible Role sap_profile_update + ansible.builtin.include_role: + name: community.sap_operations.sap_profile_update + vars: + sap_profile_update_definitions: + - sid: 'B01' + profiles: + - type: 'instance' + instance_number: '10' + path: '/usr/sap/B01/SYS/profile/B01_D10_b01hana_custom' + parameters: + - name: 'PHYS_MEMSIZE' + value: '12880' + - sid: 'B02' + profiles: + - type: 'instance' + instance_number: '20' + path: '/sapmnt/B02/profile/B02_D20_b02hana_custom' + parameters: + - name: 'PHYS_MEMSIZE' + value: '12880' +``` + +Example of removing obsolete parameter for one System `B01`. +```yaml +- name: Ansible Play for SAP Profile update + hosts: host + become: true + tasks: + - name: Execute Ansible Role sap_profile_update + ansible.builtin.include_role: + name: community.sap_operations.sap_profile_update + vars: + sap_profile_update_definitions: + - sid: 'B01' + profiles: + - type: 'default' + parameters: + - name: 'icf/user_recheck' + state: 'absent' +``` + + +## License + +Apache 2.0 + + +## Maintainers + +- [Marcel Mamula](https://github.com/marcelmamula) + + +## Role Variables + +### sap_profile_update_definitions +- _Type:_ `list` of type `list` of type `dict` + +This variable defines all SAP systems, profiles, and parameters to be managed or updated on the current host.
+It is structured as a list of dictionaries, where each top-level dictionary defines a specific SAP System ID (SID).
+ +Key fields: +* `sid`: The 3-letter SAP System ID (e.g., 'PRD', 'QAS'). +* `profiles`: A list of profile files to manage for this SID. + * `type`: `default` or `instance`. Used for dynamic path calculation. + * `instance_number`: Required if type is `instance` (e.g., '00', '10'). + * `path`: (Optional) Explicit file path to the profile. If omitted, the path + will be constructed based on 'sid', 'type', and discovered facts. + * `parameters`: The list of parameters to apply to this profile file. + * `name`: Parameter name to update (e.g., `ssl/ciphersuites`). + * `value`: New value for the parameter. Not required if state is `absent`. + * `state`: (Optional) `present` (default) or `absent`. + +Example: +```yaml +sap_profile_update_definitions: + - sid: 'B01' + profiles: + - type: 'default' + parameters: + - name: 'ssl/ciphersuites' + value: '135:PFS:HIGH::EC_P256:EC_HIGH' + - name: 'rdisp/TRACE_LOGGING' + state: 'absent' + - type: 'instance' + instance_number: '10' + path: '/usr/sap/B01/SYS/profile/B01_D10_b01hana_custom' + parameters: + - name: 'rdisp/wp_no_btc' + value: '6' + - name: 'rdisp/wp_no_vb2' + value: '2' + - sid: 'B02' + profiles: + - type: 'default' + path: '/sapmnt/B02/profile/DEFAULT.PFL' + parameters: + - name: 'ssl/ciphersuites' + value: '135:PFS:HIGH::EC_P256:EC_HIGH' +``` + +### sap_profile_update_restart_sapstartsrv +- _Type:_ `bool`
+- _Default:_ `false`
+ +Enable this variable to restart sapstartsrv service after updating parameters.
+This is applicable only for 'instance' type profiles as DEFAULT.PFL does not have sapstartsrv.
+This role does not manage restart of complete SAP System, +and this parameter is limited to `sapcontrol -nr XX -function RestartService` only.
+ diff --git a/roles/sap_profile_update/defaults/main.yml b/roles/sap_profile_update/defaults/main.yml index 442d640..c440f1f 100644 --- a/roles/sap_profile_update/defaults/main.yml +++ b/roles/sap_profile_update/defaults/main.yml @@ -1,20 +1,32 @@ -# SAP system details -sap_update_profile_sid: -sap_update_profile_instance_nr: +# SPDX-License-Identifier: Apache-2.0 +--- -# List of profile parameters to be updated in the DEFAULT profile -sap_update_profile_default_profile_params: [] -# Sample list -# sap_update_profile_default_profile_params: -# - sapgui/user_scripting = TRUE -# - ssl/ciphersuites = 135:PFS:HIGH::EC_P256:EC_HIGH -# - ssl/client_ciphersuites = 150:PFS:HIGH::EC_P256:EC_HIGH +# This variable defines all SAP systems, profiles, and parameters to be managed +# or updated on the current host. It is structured as a list of dictionaries, +# where each top-level dictionary defines a specific SAP System ID (SID). +# +# Structure: +# - List of Systems (sid) +# - List of Profiles (type: default/instance) +# - List of Parameters (name, value, state) +# +# Key fields: +# * sid: The 3-letter SAP System ID (e.g., 'PRD', 'QAS'). +# * profiles: A list of profile files to manage for this SID. +# * type: 'default' or 'instance'. Used for dynamic path calculation. +# * instance_number: Required if type is 'instance' (e.g., '00', '10'). +# * path: (Optional) Explicit file path to the profile. If omitted, the path +# will be constructed based on 'sid', 'type', and discovered facts. +# * parameters: The list of parameters to apply to this profile file. +# * name: Parameter name to update (e.g., 'ssl/ciphersuites'). +# * value: New value for the parameter. Not required if state is 'absent'. +# * state: (Optional) 'present' (default) or 'absent'. +# +sap_profile_update_definitions: [] -# List of profile parameters to be updated in the instance profile -sap_update_profile_instance_profile_params: [] -# Sample list -# sap_update_profile_instance_profile_params: -# - PHYS_MEMSIZE = 32768 -# - icm/server_port_0 = PROT=HTTP,PORT=80$$,PROCTIMEOUT=600,TIMEOUT=3600 -# - icm/server_port_1 = PROT=HTTPS,PORT=443$$,PROCTIMEOUT=600,TIMEOUT=3600 -# - icm/server_port_2 = PROT=SMTP,PORT=25$$,PROCTIMEOUT=120,TIMEOUT=120 + +# Enable this variable to restart sapstartsrv service after updating parameters. +# This is applicable only for 'instance' type profiles as DEFAULT.PFL does not have sapstartsrv. +# This role does not manage restart of complete SAP System, +# and this parameter is limited to 'sapcontrol -function RestartService' only. +sap_profile_update_restart_sapstartsrv: false diff --git a/roles/sap_profile_update/tasks/main.yml b/roles/sap_profile_update/tasks/main.yml index 1a3009b..f91ccd2 100644 --- a/roles/sap_profile_update/tasks/main.yml +++ b/roles/sap_profile_update/tasks/main.yml @@ -1,38 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 --- -# Get instance profile path using sapcontrol -- name: SAP Profile Update - Get Instance Profile - ansible.builtin.shell: | - set -o pipefail - source ~/.profile ; sapcontrol -nr {{ sap_update_profile_instance_nr }} -function ParameterValue SAPPROFILE | grep profile - args: - executable: /bin/bash - become: true - become_user: "{{ sap_update_profile_sid | lower }}adm" - register: get_instance_profile -- name: SAP Profile Update - Setup facts - ansible.builtin.set_fact: - sap_update_profile_default_profile_file_path: "/sapmnt/{{ sap_update_profile_sid }}/profile/DEFAULT.PFL" - sap_update_profile_instance_profile_file_path: "{{ get_instance_profile.stdout }}" +# Main variable 'sap_profile_update_definitions' is complex and requires special handling. +# This is achieved by dedicated task files for each level inside of list. +# 1. Systems - Keys: sid, profiles +# 2. Profiles - Keys: type, instance_number, path, parameters +# 3. Parameters - Keys: name, value, state -# Update default profile -- name: SAP Profile Update - Updating DEFAULT.PFL - ansible.builtin.include_tasks: update_parameter.yml - vars: - passed_parameter_path: "{{ sap_update_profile_default_profile_file_path }}" - loop: "{{ sap_update_profile_default_profile_params }}" - loop_control: - loop_var: passed_parameter - when: - - sap_update_profile_default_profile_params is defined +# 1. Validate through 'sap_profile_update_definitions' +# and check against host to ensure we catch failure before doing changes. +- name: SAP Profile Update - Validate variables + ansible.builtin.include_tasks: + file: validation/validate_variables.yml -# Update instance profile -- name: SAP Profile Update - Updating instance profile - ansible.builtin.include_tasks: update_parameter.yml - vars: - passed_parameter_path: "{{ sap_update_profile_instance_profile_file_path }}" - loop: "{{ sap_update_profile_instance_profile_params }}" +# 2. Loop through all top level System items to update profiles +# We need one less task file as loop is done in main, instead of validate_variables.yml. +- name: SAP Profile Update - Update parameters + ansible.builtin.include_tasks: + file: update/update_system.yml + loop: "{{ sap_profile_update_definitions }}" loop_control: - loop_var: passed_parameter - when: - - sap_update_profile_instance_profile_params is defined + loop_var: __sap_profile_update_system + label: "SID: {{ __sap_profile_update_system.sid }}" diff --git a/roles/sap_profile_update/tasks/update/update_parameter.yml b/roles/sap_profile_update/tasks/update/update_parameter.yml new file mode 100644 index 0000000..2836e71 --- /dev/null +++ b/roles/sap_profile_update/tasks/update/update_parameter.yml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Go through slurped profile and find line with current parameter +- name: SAP Profile Update - Find current parameter line in profile content + ansible.builtin.set_fact: + __sap_profile_update_fact_parameter_line: >- + {{ (__sap_profile_update_profile_dict['slurp'] | b64decode).splitlines() + | select('regex', '^' + (parameter_item.name | regex_escape()) + '\s*=') + | first | d('') }} + +# 2. Extract value of current parameter line or default to 'NOT_FOUND'. +- name: SAP Profile Update - Set fact for current parameter value + ansible.builtin.set_fact: + __sap_profile_update_fact_current_value: >- + {{ (__sap_profile_update_fact_parameter_line | regex_replace('^[^=]+=\s*([^#]+).*$', '\1') | trim) + if __sap_profile_update_fact_parameter_line + else 'NOT_FOUND' }} + +# 3. Add or Update parameter when state is 'present'. +# Audit comment is added when change is required. +# Example of adding new parameter: +# 2025-12-09 09:58:34 'rdisp/wp_no_btc' added with value '6' by Ansible Role community.sap_operations.sap_profile_update. +# Example of updating existing parameter: +# 2025-12-09 10:00:25 'rdisp/wp_no_btc' changed from '6' to '7' by Ansible Role community.sap_operations.sap_profile_update. +- name: SAP Profile Update - Ensure parameter is present + ansible.builtin.lineinfile: + path: "{{ __sap_profile_update_profile_dict['path'] }}" + regexp: '^(#\s*)?{{ parameter_item.name | regex_escape() }}\s*=' + line: |- + {% if __sap_profile_update_fact_current_value != parameter_item.value %} + {% if parameter_item.name not in __sap_profile_update_profile_dict['parameters'] %} + # {{ __add_comment }} + {% else %} + # {{ __update_comment }} + {% endif %} + {{ parameter_item.name }} = {{ parameter_item.value }} + {% else %} + {{ parameter_item.name }} = {{ parameter_item.value }} + {% endif %} + backup: true + firstmatch: true + vars: + __add_comment: >- + {{ __sap_profile_update_parameter_timestamp }} + '{{ parameter_item.name }}' added with value + '{{ parameter_item.value }}' + by Ansible Role community.sap_operations.sap_profile_update. + __update_comment: >- + {{ __sap_profile_update_parameter_timestamp }} + '{{ parameter_item.name }}' changed from + '{{ __sap_profile_update_fact_current_value }}' to + '{{ parameter_item.value }}' + by Ansible Role community.sap_operations.sap_profile_update. + when: (parameter_item.state | d('present')) == 'present' + + +# 4. Comment out parameter when state is 'absent', but don't remove it for audit purposes. +# Audit comment is added when change is required. +# Example of commenting out parameter: +# 2025-12-09 10:01:04 'rdisp/wp_no_btc' commented out by Ansible Role community.sap_operations.sap_profile_update. +- name: SAP Profile Update - Comment out parameter (absent) + ansible.builtin.replace: + path: "{{ __sap_profile_update_profile_dict['path'] }}" + backup: true + regexp: '^(#\s*)?({{ parameter_item.name | regex_escape() }}\s*=\s*.*)$' + replace: | + # {{ __delete_comment }} + # \2 + vars: + __delete_comment: >- + {{ __sap_profile_update_parameter_timestamp }} + '{{ parameter_item.name }}' commented out + by Ansible Role community.sap_operations.sap_profile_update. + when: + - (parameter_item.state | d('present')) == 'absent' + - __sap_profile_update_fact_current_line | length > 0 + - not __sap_profile_update_fact_current_line.startswith('#') diff --git a/roles/sap_profile_update/tasks/update/update_profile.yml b/roles/sap_profile_update/tasks/update/update_profile.yml new file mode 100644 index 0000000..d19a37e --- /dev/null +++ b/roles/sap_profile_update/tasks/update/update_profile.yml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Capture last changed time of profile file for comparison - before +- name: SAP Profile Update - Get modification time of profile before updates + ansible.builtin.stat: + path: "{{ __sap_profile_update_profile_dict['path'] }}" + register: __sap_profile_update_register_profile_before + when: + - sap_profile_update_restart_sapstartsrv | d(false) + - __sap_profile_update_profile.type == 'instance' + + +# 2. Loop through all Parameter items to manage them. +- name: SAP Profile Update - Manage parameters in loop + ansible.builtin.include_tasks: + file: update/update_parameter.yml + loop: "{{ __sap_profile_update_profile.parameters }}" + loop_control: + loop_var: parameter_item + label: >- + SID: {{ __sap_profile_update_system.sid }} | + Type: {{ __sap_profile_update_profile.type }} | + Inst: {{ __sap_profile_update_profile.instance_number | d('N/A') }} | + Param: {{ parameter_item.name }} | + State: {{ parameter_item.state | d('present') }} + + +# 3. Capture last changed time of profile file for comparison - after +- name: SAP Profile Update - Get modification time of profile after updates + ansible.builtin.stat: + path: "{{ __sap_profile_update_profile_dict['path'] }}" + register: __sap_profile_update_register_profile_after + when: + - sap_profile_update_restart_sapstartsrv | d(false) + - __sap_profile_update_profile.type == 'instance' + + +# 4. Restart sapstartsrv when profile was changed for instance profile, not default. +- name: SAP Profile Update - Restart sapstartsrv if profile file was modified + ansible.builtin.command: + cmd: "/usr/sap/hostctrl/exe/sapcontrol -nr {{ __sap_profile_update_profile.instance_number }} -function RestartService" + when: + - sap_profile_update_restart_sapstartsrv | d(false) + - __sap_profile_update_profile.type == 'instance' + - __sap_profile_update_register_profile_before.stat.mtime != __sap_profile_update_register_profile_after.stat.mtime + become: true diff --git a/roles/sap_profile_update/tasks/update/update_system.yml b/roles/sap_profile_update/tasks/update/update_system.yml new file mode 100644 index 0000000..31570d4 --- /dev/null +++ b/roles/sap_profile_update/tasks/update/update_system.yml @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Loop through all Profile items again to update their parameters. +- name: SAP Profile Update - Loop through each profile to update parameters + ansible.builtin.include_tasks: + file: update/update_profile.yml + vars: + __sap_profile_update_profile_dict: > + {{ + __sap_profile_update_fact_profile_dict + [__sap_profile_update_system.sid] + [__sap_profile_update_profile.instance_number | d('default')] + }} + # Timestamp format: 2025-12-09 09:58:34 + __sap_profile_update_parameter_timestamp: >- + {{ ansible_facts['date_time']['date'] }} {{ ansible_facts['date_time']['time'] }} + loop: "{{ __sap_profile_update_system.profiles }}" + loop_control: + loop_var: __sap_profile_update_profile + label: >- + SID: {{ __sap_profile_update_system.sid }} | + Type: {{ __sap_profile_update_profile.type }} | + Inst: {{ __sap_profile_update_profile.instance_number | d('N/A') }} diff --git a/roles/sap_profile_update/tasks/update_parameter.yml b/roles/sap_profile_update/tasks/update_parameter.yml deleted file mode 100644 index 9b3afde..0000000 --- a/roles/sap_profile_update/tasks/update_parameter.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -# passed_parameter.split()[0] = first word of passed_parameter used as regex -# Add comment using: -# line: "# Updated by sap_profile_update - {{ lookup('pipe','date \"+%Y-%m-%d %H:%M\"') }}\n{{ passed_parameter }}" - -- name: Update "{{ passed_parameter }}" - ansible.builtin.lineinfile: - path: "{{ passed_parameter_path }}" - regexp: "^{{ passed_parameter.split()[0] }}" - line: "{{ passed_parameter }}" - owner: "{{ sap_update_profile_sid | lower }}adm" - group: sapsys - mode: "0644" diff --git a/roles/sap_profile_update/tasks/validation/validate_profile.yml b/roles/sap_profile_update/tasks/validation/validate_profile.yml new file mode 100644 index 0000000..82b2092 --- /dev/null +++ b/roles/sap_profile_update/tasks/validation/validate_profile.yml @@ -0,0 +1,252 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Loop through all parameters under Profile and validate all required keys. +- name: SAP Profile Update - Assert that all nested parameters are valid + ansible.builtin.assert: + that: + # Parameter 'name' is valid + - parameter_item.name is defined + - parameter_item.name is string + - parameter_item.name | trim | length > 0 + + # Parameter 'state' is valid (optional, but if defined, must be 'present' or 'absent') + - (parameter_item.state is not defined) or (parameter_item.state | lower in ['present', 'absent']) + + # Parameter 'value' is valid (required only if state is present/default) + - >- + (parameter_item.state | d('present') | lower == 'absent') + or + (parameter_item.value is defined and parameter_item.value is string) + + success_msg: | + PASS: Parameter definition for '{{ parameter_item.name | d('unknown') }}' is valid. + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }}" + + fail_msg: | + {% if parameter_item.name is not defined or parameter_item.name | trim | length == 0 %} + FAIL: A parameter item is missing the 'name' key or its empty. + {% elif parameter_item.state is defined and parameter_item.state | lower not in ['present', 'absent'] %} + FAIL: Parameter 'state' must be 'present' or 'absent'. Value found: {{ parameter_item.state }} + {% elif (parameter_item.state | d('present') | lower == 'present') and parameter_item.value is not defined %} + FAIL: Parameter 'value' is required when 'state' is 'present' (default), but it is missing. + {% elif (parameter_item.state | d('present') | lower == 'present') and parameter_item.value is not string %} + FAIL: Parameter 'value' must be a string. Type found: {{ parameter_item.value | type_debug }} + {% else %} + FAIL: An unspecified parameter validation failed for name: {{ parameter_item.name | d('UNKNOWN') }} + {% endif %} + Parameter: {{ parameter_item.name | d('UNKNOWN') }} + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} + + quiet: true + loop: "{{ __sap_profile_update_validate_profile.parameters }}" + loop_control: + loop_var: parameter_item + label: >- + SID: {{ __sap_profile_update_validate_system.sid }} | + Type: {{ __sap_profile_update_validate_profile.type }} | + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} | + Parameter: {{ parameter_item.name | d('UNKNOWN') }} + + +# 2. Validate instance profile path using sapcontrol and assert if 'path' is not defined. +- name: Block for 'instance' type + when: __sap_profile_update_validate_profile.type == 'instance' + block: + - name: SAP Profile Update - Assert that sapcontrol executable exists + ansible.builtin.assert: + that: + - __sap_profile_update_register_sapcontrol_stat.stat.exists + fail_msg: | + FAIL: The executable '/usr/sap/hostctrl/exe/sapcontrol' does not exist. + Please check that the SAP HANA installation path is correct and that SAP HANA is installed properly. + + - name: SAP Profile Update - Get Instance Profile path from sapcontrol + ansible.builtin.shell: + cmd: >- + /usr/sap/hostctrl/exe/sapcontrol + -nr {{ __sap_profile_update_validate_profile.instance_number }} + -function ParameterValue SAPPROFILE + register: __sap_profile_update_register_check_sapprofile + changed_when: false + failed_when: false + + - name: SAP Profile Update - Set fact for system defined instance profile path + ansible.builtin.set_fact: + __sap_profile_update_fact_check_instance_profile_path: + "{{ __sap_profile_update_register_check_sapprofile.stdout_lines | select('search', 'profile') | first | trim }}" + when: not __sap_profile_update_register_check_sapprofile.failed + + - name: SAP Profile Update - Check presence of system defined instance profile path + ansible.builtin.stat: + path: "{{ __sap_profile_update_fact_check_instance_profile_path }}" + register: __sap_profile_update_register_system_path_stat + when: + - __sap_profile_update_validate_profile.path is not defined + - not __sap_profile_update_register_check_sapprofile.failed + + - name: SAP Profile Update - Fail if system defined instance profile path does not exist + ansible.builtin.fail: + msg: | + FAIL: Detection of instance profile path has failed, + because of invalid output of `sapcontrol` command or profile missing. + + Path: {{ __sap_profile_update_fact_check_instance_profile_path | d('N/A') }} + + Command output: + {{ __sap_profile_update_register_check_sapprofile.stdout_lines }} + when: + - __sap_profile_update_validate_profile.path is not defined + - __sap_profile_update_register_check_sapprofile.failed + or not __sap_profile_update_register_system_path_stat.stat.exists + + +# 3. Validate default profile path and assert if 'path' is not defined. +- name: Block for 'default' type + when: + - __sap_profile_update_validate_profile.type == 'default' + block: + - name: SAP Profile Update - Set fact for default profile path + ansible.builtin.set_fact: + __sap_profile_update_fact_check_default_profile_path: + "/sapmnt/{{ __sap_profile_update_validate_system.sid }}/profile/DEFAULT.PFL" + + - name: SAP Profile Update - Check presence of default profile path + ansible.builtin.stat: + path: "{{ __sap_profile_update_fact_check_default_profile_path }}" + register: __sap_profile_update_register_check_default_path + + - name: SAP Profile Update - Assert that default profile path exists + ansible.builtin.assert: + that: + - __sap_profile_update_register_check_default_path.stat.exists + fail_msg: | + FAIL: The default profile does not exist in standard path. + Ensure that SAP is installed or custom path is provided with 'path' parameter. + + Path: {{ __sap_profile_update_fact_check_default_profile_path }} + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + when: __sap_profile_update_validate_profile.path is not defined + + +# 4. Validate user defined profile path and compare it to system defined for instance type. +- name: Block to validate user defined profile path + when: __sap_profile_update_validate_profile.path is defined + block: + + - name: SAP Profile Update - Check presence of user defined profile path + ansible.builtin.stat: + path: "{{ __sap_profile_update_validate_profile.path }}" + register: __sap_profile_update_register_check_defined_path + + - name: SAP Profile Update - Assert that user defined profile path exists + ansible.builtin.assert: + that: + - __sap_profile_update_register_check_defined_path.stat.exists + fail_msg: | + FAIL: The path defined in parameter 'path' does not exist. + Please correct this value or remove the parameter 'path'. + + Path: {{ __sap_profile_update_validate_profile.path }} + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} + + - name: SAP Profile Update - Inform that user defined profile path differs to system defined + ansible.builtin.debug: + msg: | + WARN: The path defined in parameter 'path' differs to system defined. + The role will continue with further validations and changes. + + Defined: {{ __sap_profile_update_validate_profile.path }} + System: {{ __sap_profile_update_fact_check_instance_profile_path }} + + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} + when: + - __sap_profile_update_fact_check_instance_profile_path is defined + - __sap_profile_update_validate_profile.type == 'instance' + - __sap_profile_update_register_system_path_stat.stat.exists is defined + - __sap_profile_update_register_system_path_stat.stat.exists + + # Overwrite existing fact with final valid path + - name: SAP Profile Update - Set fact for user defined instance profile path + ansible.builtin.set_fact: + __sap_profile_update_fact_check_custom_profile_path: + "{{ __sap_profile_update_validate_profile.path }}" + + +# 5. Get contents of profile and validate it for duplicated parameters. +# This is done to ensure that lineinfile replacement does not fail. +- name: SAP Profile Update - Set fact for profile path + ansible.builtin.set_fact: + __sap_profile_update_fact_check_profile_path: >- + {%- if __sap_profile_update_fact_check_custom_profile_path is defined -%} + {{ __sap_profile_update_fact_check_custom_profile_path }} + {%- elif __sap_profile_update_validate_profile.type == 'instance' -%} + {{ __sap_profile_update_fact_check_instance_profile_path }} + {%- else -%} + {{ __sap_profile_update_fact_check_default_profile_path }} + {%- endif -%} + +- name: SAP Profile Update - Read profile content + ansible.builtin.slurp: + src: "{{ __sap_profile_update_fact_check_profile_path }}" + register: __sap_profile_update_register_check_profile_content + +# Extract parameter names from profile content, without comments. +- name: SAP Profile Update - Extract parameter names from profile content + ansible.builtin.set_fact: + __sap_profile_update_fact_parameter_names: >- + {{ __sap_profile_update_register_check_profile_content.content | b64decode + | regex_findall('^([a-zA-Z0-9./_]+)\s*=', multiline=True) }} + +- name: SAP Profile Update - Check for duplicated parameters in profile + ansible.builtin.assert: + that: + - __sap_profile_update_fact_parameter_names | unique | length == __sap_profile_update_fact_parameter_names | length + fail_msg: | + FAIL: The profile contains duplicated parameters. + Please review the profile and remove any duplicates. + + Profile: {{ __sap_profile_update_fact_check_profile_path }} + SID: {{ __sap_profile_update_validate_system.sid }} + Type: {{ __sap_profile_update_validate_profile.type }} + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} + + Duplicated parameters found: + {% set counts = {} %} + {% for item in __sap_profile_update_fact_parameter_names %} + {% if counts.update({item: (counts[item] | d(0)) + 1}) %}{% endif %} + {% endfor %} + {% for key, value in counts.items() if value > 1 %} + - {{ key }} + {% endfor %} + + +# 6. Build detailed dictionary that will be used to store gathered details, +# to avoid gathering them again during update loops. +# Contents can be queried with '__sap_profile_update_fact_profile_dict['B01']['default']['parameters']' +- name: SAP Profile Update - Update facts dictionary with profile details + ansible.builtin.set_fact: + __sap_profile_update_fact_profile_dict: + "{{ __sap_profile_update_fact_profile_dict | d({}) | combine(profile_fragment, recursive=True) }}" + vars: + profile_fragment: > + {{ + { + (__sap_profile_update_validate_system.sid): { + (__sap_profile_update_validate_profile.instance_number | d('default')): { + "path": __sap_profile_update_fact_check_profile_path, + "parameters": __sap_profile_update_fact_parameter_names | unique, + "slurp": __sap_profile_update_register_check_profile_content.content + } + } + } + }} diff --git a/roles/sap_profile_update/tasks/validation/validate_system.yml b/roles/sap_profile_update/tasks/validation/validate_system.yml new file mode 100644 index 0000000..6075e89 --- /dev/null +++ b/roles/sap_profile_update/tasks/validation/validate_system.yml @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Loop through all Profile items under System and validate all required keys. +- name: SAP Profile Update - Assert Profile-Level details (Type, Instance Number, Parameters List) + ansible.builtin.assert: + that: + # Type must be defined and one of the allowed values + - profile_item.type is defined + - profile_item.type is string + - profile_item.type | lower in ['default', 'instance'] + + # 'instance_number' requirements + - >- + (profile_item.type | lower == 'default') + or + (profile_item.type | lower == 'instance' + and profile_item.instance_number is defined + and profile_item.instance_number is string + and profile_item.instance_number | trim | length == 2 + and profile_item.instance_number is match('^[0-9]{2}$')) + + # Parameters must be defined as non-empty list + - profile_item.parameters is defined + - profile_item.parameters | type_debug == 'list' + - profile_item.parameters | length > 0 + + # Parameter 'path' is a string (optional) + - (profile_item.path is not defined) or (profile_item.path is string) + + success_msg: | + PASS: Profile definition of '{{ profile_item.type | d('UNKNOWN') }}' is valid. + SID: {{ __sap_profile_update_validate_system.sid }} + fail_msg: | + {% if profile_item.type is not defined or profile_item.type is not string %} + FAIL: A profile item is missing the 'type' key or its not a string type. + {% elif profile_item.type | lower not in ['default', 'instance'] %} + FAIL: Parameter 'type' must be one of values: 'default', 'instance'. + Current value: {{ profile_item.type }} + {% elif profile_item.path is defined and profile_item.path is not string %} + FAIL: Parameter 'path' must be a string, but it is of type '{{ profile_item.path | type_debug }}' + {% elif profile_item.parameters is not defined or profile_item.parameters | length == 0 %} + FAIL: A profile item is missing the 'parameters' key or its empty. + {% elif profile_item.parameters | type_debug != 'list' %} + FAIL: Parameter 'profiles' must be a list, but it is of type '{{ profile_item.parameters | type_debug }}' + {% else %} + FAIL: Parameter 'instance_number' must be a string consisting of 2 digits. + {% endif %} + Type: {{ profile_item.type | d('UNKNOWN') }} + Inst: {{ profile_item.instance_number | d('N/A') }} + SID: {{ __sap_profile_update_validate_system.sid }} + + quiet: true + loop: "{{ __sap_profile_update_validate_system.profiles }}" + loop_control: + loop_var: profile_item + label: "Type: {{ profile_item.type | d('UNKNOWN') }} Inst: {{ profile_item.instance_number | d('N/A') }}" + + +# 3. Assert that only one 'default' profile is defined per SID. +- name: SAP Profile Update - Assert that only one 'default' profile is defined per SID + ansible.builtin.assert: + that: + - __sap_profile_update_validate_system.profiles | selectattr('type', 'equalto', 'default') | list | length <= 1 + fail_msg: | + FAIL: More than one profile with type 'default' is defined for SID '{{ __sap_profile_update_validate_system.sid }}'. + Please define only one 'default' profile per SID. + + +# 4. Assert that 'instance' profiles have unique instance numbers. +- name: SAP Profile Update - Set fact for instance profile numbers + ansible.builtin.set_fact: + __sap_profile_update_fact_instance_numbers: >- + {{ __sap_profile_update_validate_system.profiles + | selectattr('type', 'equalto', 'instance') + | map(attribute='instance_number') + | list }} + +- name: SAP Profile Update - Assert that 'instance' profiles have unique instance numbers + ansible.builtin.assert: + that: + - __sap_profile_update_fact_instance_numbers | unique | length == __sap_profile_update_fact_instance_numbers | length + fail_msg: | + FAIL: Duplicate 'instance' profiles found for SID '{{ __sap_profile_update_validate_system.sid }}'. + Each profile with type 'instance' must have a unique 'instance_number'. + + Duplicated instance numbers found: + {% set counts = {} %} + {% for item in __sap_profile_update_fact_instance_numbers %} + {% if counts.update({item: (counts[item] | d(0)) + 1}) %}{% endif %} + {% endfor %} + {% for key, value in counts.items() if value > 1 %} + - {{ key }} + {% endfor %} + +# 5. Loop through all Profile items again to validate their keys and parameters. +# We loop using same list so we can retain 'SID' and 'Type' inside of loop, +# which would not be possible if we flattened list or used 'sap_profile_update_definitions.profiles.parameters'. +- name: SAP Profile Update - Loop through each profile to validate parameters + ansible.builtin.include_tasks: + file: validation/validate_profile.yml + loop: "{{ __sap_profile_update_validate_system.profiles }}" + loop_control: + loop_var: __sap_profile_update_validate_profile + label: >- + SID: {{ __sap_profile_update_validate_system.sid }} | + Type: {{ __sap_profile_update_validate_profile.type }} | + Inst: {{ __sap_profile_update_validate_profile.instance_number | d('N/A') }} diff --git a/roles/sap_profile_update/tasks/validation/validate_variables.yml b/roles/sap_profile_update/tasks/validation/validate_variables.yml new file mode 100644 index 0000000..38c333e --- /dev/null +++ b/roles/sap_profile_update/tasks/validation/validate_variables.yml @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# 1. Loop through all top level System items and validate all required keys. +- name: SAP Profile Update - Assert that the variable 'sap_profile_update_definitions' is defined as a list + ansible.builtin.assert: + that: + - sap_profile_update_definitions is defined + - sap_profile_update_definitions | type_debug == 'list' + success_msg: | + PASS: The 'sap_profile_update_definitions' variable is a list. + fail_msg: | + FAIL: The 'sap_profile_update_definitions' variable must be a list, + but it is of type '{{ sap_profile_update_definitions | type_debug }}'. + + +- name: SAP Profile Update - Assert that the variable 'sap_profile_update_definitions' contains required keys + ansible.builtin.assert: + that: + # SID must be defined as string of 3 letters + - definition_item.sid is defined + - definition_item.sid is string + - definition_item.sid | trim | length == 3 + + # Profiles must be defined as non-empty list + - definition_item.profiles is defined + - definition_item.profiles | type_debug == 'list' + - definition_item.profiles | length > 0 + + success_msg: | + PASS: Definition of '{{ definition_item.sid }}' contains 'sid' and 'profiles' keys." + + fail_msg: | + {% if definition_item.sid is not defined or definition_item.sid | length == 0 %} + FAIL: A definition is missing the 'sid' key or its empty. + {% elif definition_item.sid is not string %} + FAIL: Parameter 'sid' must be a string,but it is of type '{{ definition_item.sid | type_debug }}' + {% elif definition_item.profiles is not defined or definition_item.profiles | length == 0 %} + FAIL: A definition is missing the 'profiles' key or its empty. + {% else %} + FAIL: Parameter 'profiles' must be a list, but it is of type '{{ definition_item.profiles | type_debug }}' + {% endif %} + + quiet: true + loop: "{{ sap_profile_update_definitions }}" + loop_control: + loop_var: definition_item + label: "{{ definition_item.sid | d('UNKNOWN') }}" + + +# 2. Check presence of sapcontrol executable +- name: SAP Profile Update - Check presence of sapcontrol executable + ansible.builtin.stat: + path: "/usr/sap/hostctrl/exe/sapcontrol" + register: __sap_profile_update_register_sapcontrol_stat + + +# 3. Loop through all top level System items again to validate their Profile items. +# We loop using same list so we can retain 'SID' inside of loop, +# which would not be possible if we flattened list or used 'sap_profile_update_definitions.profiles'. +- name: SAP Profile Update - Loop through each System (SID) to validate profile-level details + ansible.builtin.include_tasks: + file: validation/validate_system.yml + loop: "{{ sap_profile_update_definitions }}" + loop_control: + loop_var: __sap_profile_update_validate_system + label: "SID: {{ __sap_profile_update_validate_system.sid }}"