From 28ca6713b640277161d4592db10746b0474136ae Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Thu, 7 May 2026 16:56:35 +0200 Subject: [PATCH 1/4] SWPM pseudo-idempotency implementation initial commit --- roles/sap_swpm/defaults/main.yml | 4 + roles/sap_swpm/tasks/main.yml | 15 +- roles/sap_swpm/tasks/post_install.yml | 75 ++-- .../post_install/set_password_expiration.yml | 51 +++ roles/sap_swpm/tasks/pre_install.yml | 234 +++++++----- .../sap_swpm/tasks/pre_install/detect_sap.yml | 351 ++++++++++++++++++ .../tasks/pre_install/install_type.yml | 34 +- roles/sap_swpm/vars/main.yml | 102 +++++ 8 files changed, 712 insertions(+), 154 deletions(-) create mode 100644 roles/sap_swpm/tasks/post_install/set_password_expiration.yml create mode 100644 roles/sap_swpm/tasks/pre_install/detect_sap.yml diff --git a/roles/sap_swpm/defaults/main.yml b/roles/sap_swpm/defaults/main.yml index 3e0903bd..5224e715 100644 --- a/roles/sap_swpm/defaults/main.yml +++ b/roles/sap_swpm/defaults/main.yml @@ -508,3 +508,7 @@ sap_swpm_display_unattended_output: false # Set which Ansible Collection to use when calling sap_install roles. sap_swpm_sap_install_collection: 'community.sap_install' + +# Optional override that avoids failures if existing SAP instance is detected. +# This variable should be used with caution as running SWPM installation with active SAP instances can lead to corruption of existing SAP installation. +sap_swpm_force: false diff --git a/roles/sap_swpm/tasks/main.yml b/roles/sap_swpm/tasks/main.yml index 0b179883..2bf6fd3d 100644 --- a/roles/sap_swpm/tasks/main.yml +++ b/roles/sap_swpm/tasks/main.yml @@ -40,17 +40,22 @@ tags: always - name: SAP SWPM - Run pre_install tasks - ansible.builtin.import_tasks: pre_install.yml + ansible.builtin.include_tasks: + file: pre_install.yml tags: - sap_swpm_pre_install - sap_swpm_install - name: SAP SWPM - Run swpm - ansible.builtin.import_tasks: swpm.yml - when: sap_swpm_run_sapinst + ansible.builtin.include_tasks: + file: swpm.yml + when: + - sap_swpm_run_sapinst + - not __sap_swpm_sap_detected | d(false) or sap_swpm_force | d(false) tags: - sap_swpm_install -- name: SAP SWPM - Run postinstall task - ansible.builtin.import_tasks: post_install.yml +- name: SAP SWPM - Run post_install task + ansible.builtin.include_tasks: + file: post_install.yml when: sap_swpm_run_sapinst diff --git a/roles/sap_swpm/tasks/post_install.yml b/roles/sap_swpm/tasks/post_install.yml index c3cdc900..b4a5ce86 100644 --- a/roles/sap_swpm/tasks/post_install.yml +++ b/roles/sap_swpm/tasks/post_install.yml @@ -1,35 +1,25 @@ # SPDX-License-Identifier: Apache-2.0 --- -# Reason for noqa: The command might change things but we do not yet attempt to find out -- name: SAP SWPM Post Install - ensure password expiry disabled {{ sap_swpm_sid | lower + 'adm' }} - ansible.builtin.shell: | - chage -m 0 -M 99999 -I -1 -E -1 {{ sap_swpm_sid | lower }}adm - chage -m 0 -M 99999 -I -1 -E -1 sapadm - args: - executable: /bin/bash - become: true - register: __sap_swpm_post_install_register_sidadm_noexpire - changed_when: __sap_swpm_post_install_register_sidadm_noexpire is succeeded - when: sap_swpm_set_sidadm_noexpire | default(true) - -# Firewall steps are part of pre_tasks to ensure that ports are open before installation. +# Set user expiration update +- name: SAP SWPM Post Install - Get list of available OS users + ansible.builtin.getent: + database: passwd -######################################################################################################################## +# New task file includes detailed steps to set each parameter, instead of all at same time. +- name: SAP SWPM Post Install - Execute user expiration task file + ansible.builtin.include_tasks: + file: post_install/set_password_expiration.yml + loop: + - "{{ sap_swpm_sid | lower }}adm" + - "sapadm" + loop_control: + loop_var: user_item + when: + - user_item in ansible_facts['getent_passwd'] + - sap_swpm_set_sidadm_noexpire | d(true) -- name: SAP SWPM Deployment - Finished - ansible.builtin.debug: - msg: | - " SAP SWPM deployment successfully completed " - " " - " SAP Product - {{ sap_swpm_product_catalog_id }} " - " SID - {{ sap_swpm_sid | d('') }} " - " Primary Instance - {{ sap_swpm_pas_instance_nr | d('') }} " - " Host - {{ ansible_facts['hostname'] }} " - " FQDN - {{ ansible_facts['fqdn'] }} " - " IP - {{ ansible_facts['default_ipv4'].address | d(ansible_facts['all_ipv4_addresses'][0]) }} " -# " Master Password - {{ sap_swpm_master_password }} " -# " DDIC 000 Password - {{ sap_swpm_ddic_000_password }} " +# Firewall steps are part of pre_tasks to ensure that ports are open before installation. # SAP HANA Client will not be installed for any installation with SAP AnyDB # and will only be installed alongside SAP NWAS PAS or AAS (not NWAS ASCS) @@ -55,6 +45,36 @@ register: __sap_swpm_post_install_register_hdbuserstore_connection changed_when: __sap_swpm_post_install_register_hdbuserstore_connection is succeeded + +######################################################################################################################## + +- name: SAP SWPM Deployment - Display SAP System details after completion + ansible.builtin.debug: + msg: | + SAP System or Instance is deployed. + {% if __sap_swpm_sap_detected | d(false) and not sap_swpm_force | d(false) %} + + NOTE: SAP Instance(s) were detected before installation. + SWPM installation steps were skipped to avoid potential issues with existing installation. + + If you intend to proceed with the installation despite this warning, + set 'sap_swpm_force' variable to 'true' and execute this Ansible Role again. + {% endif %} + {% if __sap_swpm_sap_detected | d(false) and sap_swpm_force | d(false) %} + + NOTE: SAP Instance(s) were detected before installation. + SWPM installation steps were executed regardless, + because 'sap_swpm_force' variable is set to 'true' (default is 'false'). + {% endif %} + + SAP Product - {{ sap_swpm_product_catalog_id }} + SID - {{ sap_swpm_sid | d('') }} + Primary Instance - {{ sap_swpm_pas_instance_nr | d('') }} + Host - {{ ansible_facts['hostname'] }} + FQDN - {{ ansible_facts['fqdn'] }} + IP - {{ ansible_facts['default_ipv4'].address | d(ansible_facts['all_ipv4_addresses'][0]) }} + + # Now that SWPM finished we may need to deal with SUM before continuing when sap_swpm_sum_start: 'true' # and if we are doing OneHost or CI/PAS installation # If observer mode is enabled, SWPM will wait for SUM to finish before continuing so we can't do anything here @@ -65,3 +85,4 @@ - not __sap_swpm_fact_observer_mode - "'NW_ABAP_CI:' in sap_swpm_product_catalog_id | string or 'NW_ABAP_OneHost:' in sap_swpm_product_catalog_id | string" + - not __sap_swpm_sap_detected | d(false) or sap_swpm_force | d(false) diff --git a/roles/sap_swpm/tasks/post_install/set_password_expiration.yml b/roles/sap_swpm/tasks/post_install/set_password_expiration.yml new file mode 100644 index 00000000..f1adbbd5 --- /dev/null +++ b/roles/sap_swpm/tasks/post_install/set_password_expiration.yml @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# NOTE: This can be replaced completely by 'ansible.builtin.user' module when we increase minimum ansible version to 2.18! +# Argument '-I' is set by module parameter 'password_expire_account_disable' added in 2.18. +# ansible.builtin.user: +# name: "{{ user_item }}" +# password_expire_min: 0 # chage -m 0 +# password_expire_max: 99999 # chage -M 99999 +# password_expire_account_disable: -1 # chage -I -1 +# expires: -1 # chage -E -1 + + +- name: SAP SWPM Post Install - Get current chage status for user {{ user_item }} + ansible.builtin.command: + cmd: chage -l {{ user_item }} + register: __sap_swpm_register_post_install_change + changed_when: false + become: true + +# Reason for noqa: Command 'chage' does not have stdout to check against so we let module decide what is considered change. +- name: SAP SWPM Post Install - Ensure min days is 0 for {{ user_item }} # noqa no-changed-when + ansible.builtin.command: + cmd: chage -m 0 {{ user_item }} + become: true + when: not __sap_swpm_register_post_install_change.stdout + | regex_search('Minimum number of days between password change\\s+:\\s+0') + +# Reason for noqa: Command 'chage' does not have stdout to check against so we let module decide what is considered change. +- name: SAP SWPM Post Install - Ensure max days is 99999 for {{ user_item }} # noqa no-changed-when + ansible.builtin.command: + cmd: chage -M 99999 {{ user_item }} + become: true + when: not __sap_swpm_register_post_install_change.stdout + | regex_search('Maximum number of days between password change\\s+:\\s+99999') + +# Reason for noqa: Command 'chage' does not have stdout to check against so we let module decide what is considered change. +- name: SAP SWPM Post Install - Ensure password inactive is disabled (-1) for {{ user_item }} # noqa no-changed-when + ansible.builtin.command: + cmd: chage -I -1 {{ user_item }} + become: true + when: not __sap_swpm_register_post_install_change.stdout + | regex_search('Password inactive\\s+:\\s+never') + +# Reason for noqa: Command 'chage' does not have stdout to check against so we let module decide what is considered change. +- name: SAP SWPM Post Install - Ensure account expiration is disabled (-1) for {{ user_item }} # noqa no-changed-when + ansible.builtin.command: + cmd: chage -E -1 {{ user_item }} + become: true + when: not __sap_swpm_register_post_install_change.stdout + | regex_search('Account expires\\s+:\\s+never') diff --git a/roles/sap_swpm/tasks/pre_install.yml b/roles/sap_swpm/tasks/pre_install.yml index 52a53829..28da820e 100644 --- a/roles/sap_swpm/tasks/pre_install.yml +++ b/roles/sap_swpm/tasks/pre_install.yml @@ -88,7 +88,7 @@ - sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_parameters_dict'] is defined - sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_custom_values_dictionary'] is defined -# Unconditionally define sap_swpm_product_catalog_id from low level member. + # Unconditionally define sap_swpm_product_catalog_id from low level member. - name: SAP SWPM Pre Install - Define 'sap_swpm_product_catalog_id' from low level member with the same name ansible.builtin.set_fact: sap_swpm_product_catalog_id: "{{ sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_product_catalog_id'] }}" @@ -98,7 +98,7 @@ ansible.builtin.debug: msg: "sap_swpm_product_catalog_id: '{{ sap_swpm_product_catalog_id }}'" -# Unconditionally define sap_swpm_inifile_sections_list from low level member. + # Unconditionally define sap_swpm_inifile_sections_list from low level member. - name: SAP SWPM Pre Install - Define 'sap_swpm_inifile_sections_list' from low level member ansible.builtin.set_fact: sap_swpm_inifile_sections_list: "{{ sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_list'] | @@ -107,7 +107,7 @@ sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_sections_list'] is defined no_log: true -# Define sap_swpm_role_parameters_dict from low level member. + # Define sap_swpm_role_parameters_dict from low level member. - name: SAP SWPM Pre Install - Define 'sap_swpm_role_parameters_dict' from low level member ansible.builtin.set_fact: sap_swpm_role_parameters_dict: "{{ sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_dictionary'] | @@ -124,8 +124,8 @@ - sap_swpm_role_parameters_dict | length > 0 ignore_errors: true -# Define role variables from low level member, looping over 'sap_swpm_role_parameters_dict'. -# Reason for noqa: We are setting variables from the content of a dict, and the variable names are in the dict keys. + # Define role variables from low level member, looping over 'sap_swpm_role_parameters_dict'. + # Reason for noqa: We are setting variables from the content of a dict, and the variable names are in the dict keys. - name: SAP SWPM Pre Install - Define role variables from low level member, looping over 'sap_swpm_role_parameters_dict' # noqa var-naming[no-jinja] ansible.builtin.set_fact: "{{ __sap_swpm_role_parameters_dict_line_item.key }}": "{{ __sap_swpm_role_parameters_dict_line_item.value }}" @@ -137,7 +137,7 @@ - sap_swpm_role_parameters_dict | length > 0 no_log: true -# Define sap_swpm_inifile_parameters_dict from low level member, or use top level variable if defined. Low level definition has precedence. + # Define sap_swpm_inifile_parameters_dict from low level member, or use top level variable if defined. Low level definition has precedence. - name: SAP SWPM Pre Install - Define 'sap_swpm_inifile_parameters_dict' from low level member ansible.builtin.set_fact: sap_swpm_inifile_parameters_dict: "{{ sap_swpm_templates_install_dictionary[sap_swpm_templates_product_input]['sap_swpm_inifile_custom_values_dictionary'] | @@ -271,11 +271,11 @@ ansible.builtin.fail: msg: "Validation failed - Please configure the failed role variables correctly and restart!" + # Assert correct hostname and IP resolution of virtual hostname and IP if: # - sapinst is requested to be run, and # - /etc/hosts file is requested to be updated, and # - sap_swpm_virtual_hostname has been defined - - name: SAP SWPM Pre Install - Assert hostname resolution for HA ansible.builtin.include_tasks: file: pre_install/assert_hostname_resolution_for_ha.yml @@ -288,107 +288,145 @@ - sap_swpm_virtual_hostname | length > 0 tags: sap_swpm_update_etchosts -################ -# Run Preinstallation Steps Based on Run Mode -################ -- name: SAP SWPM Pre Install - Run Preinstallation Steps - ansible.builtin.include_tasks: pre_install/swpm_prepare.yml - tags: - - sap_swpm_generate_inifile - - sap_swpm_sapinst_commandline - -################ -# Pre Install Optional Tasks -################ +- name: SAP SWPM Pre Install - Set fact with Product ID prefix + ansible.builtin.set_fact: + __sap_swpm_fact_product_prefix: "{{ sap_swpm_product_catalog_id.split(':') | first }}" + when: + - sap_swpm_product_catalog_id | d('') | trim | length > 0 + - "':' in sap_swpm_product_catalog_id" -# Firewall -- name: SAP SWPM Pre Install - Configure Firewall +# While SWPM allows providing extra inputs and ignores them, +# we want to ensure that some sections are provided before failure occurs inside of SWPM execution. +- name: SAP SWPM Pre Install - Assert that inifile sections are provided for product + ansible.builtin.assert: + that: >- + (__sap_swpm_product_sections_dict[__product_prefix]['sections'] + | intersect(sap_swpm_inifile_sections_list) | length) + == __sap_swpm_product_sections_dict[__product_prefix]['sections'] | length + fail_msg: | + FAIL: Product '{{ sap_swpm_product_catalog_id }}' requires + all of the following sections to be defined in 'sap_swpm_inifile_sections_list': + {{ __sap_swpm_product_sections_dict[__product_prefix]['sections'] | join(', ') }}. + vars: + __product_prefix: "{{ sap_swpm_product_catalog_id.split(':') | first }}" + when: + - sap_swpm_inifile_sections_list | d([]) | length > 0 + - sap_swpm_product_catalog_id | d('') | trim | length > 0 + - "':' in sap_swpm_product_catalog_id" + - __sap_swpm_product_sections_dict[__product_prefix] is defined + +# Detect running SAP instances using variables validated in tasks above. +# NOTE: This will result in skipping of remaining tasks, unless force is used. +- name: SAP SWPM Pre Install - Detect running SAP instances ansible.builtin.include_tasks: - file: pre_install/firewall.yml + file: pre_install/detect_sap.yml apply: + tags: always + tags: always + + +- name: Block to execute on empty host or in force mode + when: not __sap_swpm_sap_detected | d(false) or sap_swpm_force | d(false) + block: + + # Run Pre-installation Steps Based on Run Mode + - name: SAP SWPM Pre Install - Run Pre-installation Steps + ansible.builtin.include_tasks: pre_install/swpm_prepare.yml + tags: + - sap_swpm_generate_inifile + - sap_swpm_sapinst_commandline + + + ## Pre Install Optional Tasks + # Firewall + - name: SAP SWPM Pre Install - Configure Firewall + ansible.builtin.include_tasks: + file: pre_install/firewall.yml + apply: + tags: sap_swpm_configure_firewall + when: + - sap_swpm_configure_firewall tags: sap_swpm_configure_firewall - when: - - sap_swpm_configure_firewall - tags: sap_swpm_configure_firewall -# /etc/hosts + # /etc/hosts -- name: SAP SWPM Pre Install - Update /etc/hosts - ansible.builtin.include_tasks: - file: pre_install/update_etchosts.yml - apply: + - name: SAP SWPM Pre Install - Update /etc/hosts + ansible.builtin.include_tasks: + file: pre_install/update_etchosts.yml + apply: + tags: sap_swpm_update_etchosts + when: + - sap_swpm_run_sapinst + - sap_swpm_update_etchosts tags: sap_swpm_update_etchosts - when: - - sap_swpm_run_sapinst - - sap_swpm_update_etchosts - tags: sap_swpm_update_etchosts -################ -# Display Parameters -################ -- name: SAP SWPM - Display Software - ansible.builtin.debug: - msg: | - SAPCAR: {{ __sap_swpm_fact_sapcar_path }}: {{ __sap_swpm_fact_sapcar_file_name }} - SWPM: {{ __sap_swpm_fact_swpm_path }}: {{ __sap_swpm_fact_swpm_file_name }} - {% if not sap_swpm_generic %} - SAPEXEDB: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapexedb_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} - SAPEXE: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapexe_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} - IGS: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_igs_exe_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} - IGS Helper: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_igs_helper_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} - {% if 'Webdispatcher' in sap_swpm_product_catalog_id | string %} - WEBDISP: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapwebdisp_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} - {% endif %} - {% endif %} - {% if __sap_swpm_fact_installation_type in ['maint_plan_stack', 'ha_maint_plan_stack'] %} - MP Stack: {{ (sap_swpm_mp_stack_path | d('', true) + '/' + sap_swpm_mp_stack_file_name | d('', true)) | regex_replace('//*', '/') }}" - {% endif %} - {% if sap_swpm_backup_location is defined and sap_swpm_backup_location | string | trim | length > 0 %} - Backup: {{ sap_swpm_backup_location }} - {% endif %} + # Display Parameters + - name: SAP SWPM - Display Software + ansible.builtin.debug: + msg: | + SAPCAR: {{ __sap_swpm_fact_sapcar_path }}: {{ __sap_swpm_fact_sapcar_file_name }} + SWPM: {{ __sap_swpm_fact_swpm_path }}: {{ __sap_swpm_fact_swpm_file_name }} + {% if not sap_swpm_generic %} + SAPEXEDB: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapexedb_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} + SAPEXE: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapexe_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} + IGS: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_igs_exe_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} + IGS Helper: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_igs_helper_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} + {% if 'Webdispatcher' in sap_swpm_product_catalog_id | string %} + WEBDISP: {{ __sap_swpm_fact_software_path }}: {{ __sap_swpm_register_sapwebdisp_files.files | map(attribute='path') | map('basename') | join(', ') | d('') }} + {% endif %} + {% endif %} + {% if __sap_swpm_fact_installation_type in ['maint_plan_stack', 'ha_maint_plan_stack'] %} + MP Stack: {{ (sap_swpm_mp_stack_path | d('', true) + '/' + sap_swpm_mp_stack_file_name | d('', true)) | regex_replace('//*', '/') }}" + {% endif %} + {% if sap_swpm_backup_location is defined and sap_swpm_backup_location | string | trim | length > 0 %} + Backup: {{ sap_swpm_backup_location }} + {% endif %} + + + - name: SAP SWPM - Installation Process + ansible.builtin.debug: + msg: | + Ready to run SWPM for SAP Software Installation + - SAP Product - {{ sap_swpm_product_catalog_id }} + - SID - {{ sap_swpm_sid }} + - Host - {{ ansible_facts['hostname'] }} -- name: SAP SWPM - Installation Process - ansible.builtin.debug: - msg: - - " Ready to run SWPM for SAP Software Installation " - - " " - - " SAP Product - {{ sap_swpm_product_catalog_id }} " - - " SID - {{ sap_swpm_sid }} " - - " Host - {{ ansible_facts['hostname'] }} " - - " " - - " The installation can take up to 3 hours. Run the following command as root" - - " on {{ ansible_facts['hostname'] }} to display the installation logs:" - - " # tail -f $(cat {{ __sap_swpm_fact_tempdir_from_env }}/sapinst_instdir/.lastInstallationLocation)/sapinst.log" - -################ -# Set sapinst command -################ - -- name: SAP SWPM Pre Install - Set the sapinst command parameters - vars: - sap_swpm_command_guiserver: "{{ 'SAPINST_START_GUISERVER=false' if not __sap_swpm_fact_observer_mode else '' }}" - sap_swpm_command_observer: "{{ 'SAPINST_REMOTE_ACCESS_USER=' + __sap_swpm_fact_remote_access_user + ' SAPINST_REMOTE_ACCESS_USER_IS_TRUSTED=true' if __sap_swpm_fact_observer_mode and __sap_swpm_fact_remote_access_user | d('', true) | length > 0 else '' }}" - ansible.builtin.set_fact: - __sap_swpm_fact_command_inifile: "SAPINST_INPUT_PARAMETERS_URL={{ __sap_swpm_register_tmpdir.path }}/inifile.params" - __sap_swpm_fact_command_product_id: "SAPINST_EXECUTE_PRODUCT_ID={{ sap_swpm_product_catalog_id }}" - # If SWPM is running a normal install Ansible Variable __sap_swpm_fact_command_virtual_hostname is blank - # IF SWPM is running a HA installation, Ansible Variable __sap_swpm_fact_command_virtual_hostname is set and contains "SAPINST_USE_HOSTNAME={{ sap_swpm_virtual_hostname }} IS_HOST_LOCAL_USING_STRING_COMPARE=true" - # If SWPM is running a MP Stack XML installation, Ansible Variable __sap_swpm_fact_command_mp_stack is set and contains "SAPINST_STACK_XML={{ sap_swpm_mp_stack_path }} + '/' (if needed) + {{ sap_swpm_mp_stack_file_name }}" - __sap_swpm_fact_command_extra_args: "SAPINST_SKIP_DIALOGS=true {{ sap_swpm_command_guiserver }} {{ sap_swpm_command_observer }} {{ __sap_swpm_fact_command_virtual_hostname | d('') }} {{ __sap_swpm_fact_command_mp_stack | d('') }} {{ sap_swpm_command_extra_args | d('') }}" - tags: sap_swpm_sapinst_commandline - -- name: Set fact for the sapinst command line - ansible.builtin.set_fact: - __sap_swpm_sapinst_command: >- - umask {{ sap_swpm_umask | d('022') }} ; ./sapinst {{ __sap_swpm_fact_command_inifile }} - {{ __sap_swpm_fact_command_product_id }} - {{ __sap_swpm_fact_command_extra_args }} - tags: sap_swpm_sapinst_commandline + - The installation can take up to 3 hours. Run the following command as root + - on {{ ansible_facts['hostname'] }} to display the installation logs: + - tail -f $(cat {{ __sap_swpm_fact_tempdir_from_env }}/sapinst_instdir/.lastInstallationLocation)/sapinst.log -- name: Display the sapinst command line - ansible.builtin.debug: - msg: "SAP SWPM install command: '{{ __sap_swpm_sapinst_command }}'" - tags: sap_swpm_sapinst_commandline + + # Set sapinst command + - name: SAP SWPM Pre Install - Set the sapinst command parameters + vars: + sap_swpm_command_guiserver: "{{ 'SAPINST_START_GUISERVER=false' if not __sap_swpm_fact_observer_mode else '' }}" + sap_swpm_command_observer: + "{{ 'SAPINST_REMOTE_ACCESS_USER=' + __sap_swpm_fact_remote_access_user + ' SAPINST_REMOTE_ACCESS_USER_IS_TRUSTED=true' + if __sap_swpm_fact_observer_mode and __sap_swpm_fact_remote_access_user | d('', true) | length > 0 else '' }}" + ansible.builtin.set_fact: + __sap_swpm_fact_command_inifile: "SAPINST_INPUT_PARAMETERS_URL={{ __sap_swpm_register_tmpdir.path }}/inifile.params" + __sap_swpm_fact_command_product_id: "SAPINST_EXECUTE_PRODUCT_ID={{ sap_swpm_product_catalog_id }}" + # If SWPM is running a normal install Ansible Variable __sap_swpm_fact_command_virtual_hostname is blank + # IF SWPM is running a HA installation, Ansible Variable __sap_swpm_fact_command_virtual_hostname is set + # and contains "SAPINST_USE_HOSTNAME={{ sap_swpm_virtual_hostname }} IS_HOST_LOCAL_USING_STRING_COMPARE=true" + # If SWPM is running a MP Stack XML installation, Ansible Variable __sap_swpm_fact_command_mp_stack is set + # and contains "SAPINST_STACK_XML={{ sap_swpm_mp_stack_path }} + '/' (if needed) + {{ sap_swpm_mp_stack_file_name }}" + __sap_swpm_fact_command_extra_args: "SAPINST_SKIP_DIALOGS=true {{ sap_swpm_command_guiserver }} {{ sap_swpm_command_observer }} {{ + __sap_swpm_fact_command_virtual_hostname | d('') }} {{ __sap_swpm_fact_command_mp_stack | d('') }} {{ sap_swpm_command_extra_args | d('') }}" + tags: sap_swpm_sapinst_commandline + + - name: Set fact for the sapinst command line + ansible.builtin.set_fact: + __sap_swpm_sapinst_command: >- + umask {{ sap_swpm_umask | d('022') }} ; ./sapinst {{ __sap_swpm_fact_command_inifile }} + {{ __sap_swpm_fact_command_product_id }} + {{ __sap_swpm_fact_command_extra_args }} + tags: sap_swpm_sapinst_commandline + + - name: Display the sapinst command line + ansible.builtin.debug: + msg: "SAP SWPM install command: '{{ __sap_swpm_sapinst_command }}'" + tags: sap_swpm_sapinst_commandline diff --git a/roles/sap_swpm/tasks/pre_install/detect_sap.yml b/roles/sap_swpm/tasks/pre_install/detect_sap.yml new file mode 100644 index 00000000..1b7367d4 --- /dev/null +++ b/roles/sap_swpm/tasks/pre_install/detect_sap.yml @@ -0,0 +1,351 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# This is pseudo-idempotency validation to ensure that remaining SWPM tasks are skipped if there are running SAP instances detected. +# This is to prevent any potential issues that could arise from running the SWPM installation while SAP instances are active. +# Optional override is available in defaults/main.yml to execute regardless of warning. + +# Steps for checking existing SAP instances: +# 0. Prepare lists of instance names and numbers for detection based on provided variables and product catalog ID. +# 1. Check output from /usr/sap/hostctrl/exe/saphostctrl -function ListInstances +# 2. Check contents of /usr/sap/sapservices +# 3. Check contents of /usr/sap// directories +# 4. Check profile files in /sapmnt//profile/ +# 5. Set boolean flag for skipping further tasks. +# 6. Inform user about detected instances and potential risks of proceeding with installation. + + +## 0. Prepare lists of instance names and numbers for detection. +# Lists based on Product ID. +# Regex is used to remove items with empty values. +- name: SAP SWPM Pre Install - Detect - Set fact with lists for detection using product ID + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: + "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instance_numbers'] + | select('regex', '.+') | unique | list }}" + __sap_swpm_detect_instance_names: + "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instance_names'] + | select('regex', '.+') | unique | list }}" + when: + - __sap_swpm_fact_product_prefix | d('') | trim | length > 0 + - __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix] is defined + + +# Lists based on sections list if Product ID cannot be used. +# NOTE: Defaults before 'unique' do not serve any purpose, because when already validates it, +# but it is required for linting to pass: +# Falling back to Ansible unique filter as Jinja2 one failed: 'sap_swpm_ascs_instance_nr' is undefined +- name: Block to fall back on sections if product ID is not in dictionary + when: + - __sap_swpm_fact_product_prefix | d('') | trim | length == 0 + or __sap_swpm_fact_product_prefix not in __sap_swpm_product_sections_dict + - __sap_swpm_fact_product_prefix + not in __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten | unique + block: + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for ASCS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: >- + {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_ascs_instance_nr | d('')]) | unique }} + __sap_swpm_detect_instance_names: >- + {{ (__sap_swpm_detect_instance_names | d([]) + ['ASCS' ~ sap_swpm_ascs_instance_nr]) | d('') | unique }} + when: + - sap_swpm_ascs_instance_nr | d('') | length > 0 + - "'nw_config_central_services_abap' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for SCS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: >- + {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_java_scs_instance_nr | d('')]) | unique }} + __sap_swpm_detect_instance_names: >- + {{ (__sap_swpm_detect_instance_names | d([]) + ['SCS' ~ sap_swpm_java_scs_instance_nr]) | unique }} + when: + - sap_swpm_java_scs_instance_nr | d('') | length > 0 + - "'nw_config_central_services_java' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for ERS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: >- + {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_ers_instance_nr | d('')]) | unique }} + __sap_swpm_detect_instance_names: >- + {{ (__sap_swpm_detect_instance_names | d([]) + ['ERS' ~ sap_swpm_ers_instance_nr]) | unique }} + when: + - sap_swpm_ers_instance_nr | d('') | length > 0 + - "'nw_config_ers' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for PAS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: >- + {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_pas_instance_nr | d('')]) | unique }} + __sap_swpm_detect_instance_names: >- + {{ (__sap_swpm_detect_instance_names | d([]) + __pas_instance_names) | unique }} + vars: + __pas_instance_names: + - "{{ 'D' ~ sap_swpm_pas_instance_nr }}" + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr }}" + - "{{ 'J' ~ sap_swpm_pas_instance_nr }}" + when: + - sap_swpm_pas_instance_nr | d('') | length > 0 + - "'nw_config_primary_application_server_instance' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for AAS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instance_numbers: >- + {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_aas_instance_nr | d('')]) | unique }} + __sap_swpm_detect_instance_names: >- + {{ (__sap_swpm_detect_instance_names | d([]) + __aas_instance_names) | unique }} + vars: + __aas_instance_names: + - "{{ 'D' ~ sap_swpm_aas_instance_nr }}" + - "{{ 'J' ~ sap_swpm_aas_instance_nr }}" + when: + - sap_swpm_aas_instance_nr | d('') | length > 0 + - "'nw_config_additional_application_server_instance' in sap_swpm_inifile_sections_list" + + +## 1. Check output from /usr/sap/hostctrl/exe/saphostctrl -function ListInstances +- name: Block for saphostctrl checks + when: __sap_swpm_detect_instance_numbers | d([]) | length > 0 + block: + - name: SAP SWPM Pre Install - Detect - Stat /usr/sap/hostctrl/exe/saphostctrl file + ansible.builtin.stat: + path: /usr/sap/hostctrl/exe/saphostctrl + register: __sap_swpm_register_saphostctrl_stat + + - name: SAP SWPM Pre Install - Detect - Execute saphostctrl function ListInstances + ansible.builtin.command: + cmd: /usr/sap/hostctrl/exe/saphostctrl -function ListInstances + register: __sap_swpm_register_saphostctrl_listinstances + ignore_errors: true + changed_when: false + when: + - __sap_swpm_register_saphostctrl_stat.stat.exists + - __sap_swpm_register_saphostctrl_stat.stat.executable + + # Match pattern: " - ". + # Example: Inst Info : AE1 - 10 - ae1ers-ha - 793, patch 51, commit 375ae433dffb023985041c1d7b4687b1bde8d211 + - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in saphostctrl output + ansible.builtin.set_fact: + __sap_swpm_fact_sap_instances_found: >- + {%- set found = [] -%} + {%- for line in __sap_swpm_register_saphostctrl_listinstances.stdout_lines | d([]) -%} + {%- for nr in __sap_swpm_detect_instance_numbers -%} + {%- if line is search(' ' ~ sap_swpm_sid ~ ' - ' ~ nr ~ ' ') -%} + {%- set _ = found.append(nr) -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{ found | unique }} + + - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in saphostctrl + ansible.builtin.debug: + msg: >- + WARN: Existing SAP instances detected in saphostctrl output for {{ sap_swpm_sid }}: + {{ __sap_swpm_fact_sap_instances_found | join(', ') }} + when: __sap_swpm_fact_sap_instances_found | length > 0 + + +## 2. Check contents of /usr/sap/sapservices +# This file can be managed by cluster software like Veritas, +# therefore not showing instance if cluster is stopped. +- name: Block for sapservices checks + when: __sap_swpm_detect_instance_numbers | d([]) | length > 0 + block: + - name: SAP SWPM Pre Install - Detect - Stat /usr/sap/sapservices file + ansible.builtin.stat: + path: /usr/sap/sapservices + register: __sap_swpm_register_sapservices_stat + + - name: SAP SWPM Pre Install - Detect - Read /usr/sap/sapservices file + ansible.builtin.slurp: + path: /usr/sap/sapservices + register: __sap_swpm_register_sapservices_slurp + when: __sap_swpm_register_sapservices_stat.stat.exists + + # Match pattern: SAP_ + - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in sapservices file + ansible.builtin.set_fact: + __sap_swpm_fact_sap_services_found: >- + {%- set content = (__sap_swpm_register_sapservices_slurp.content | b64decode) + if __sap_swpm_register_sapservices_slurp.content is defined else '' -%} + {%- set found = [] -%} + {%- for line in content.splitlines() -%} + {%- for nr in __sap_swpm_detect_instance_numbers -%} + {%- if ('SAP' ~ sap_swpm_sid ~ '_' ~ nr) in line -%} + {%- set _ = found.append(nr) -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{ found | unique }} + + - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in sapservices + ansible.builtin.debug: + msg: >- + WARN: Existing SAP instances detected in sapservices file for {{ sap_swpm_sid }}: + {{ __sap_swpm_fact_sap_services_found | join(', ') }} + when: __sap_swpm_fact_sap_services_found | length > 0 + + +## 3. Check contents of /usr/sap// directories +- name: Block for directories checks + when: __sap_swpm_detect_instance_names | d([]) | length > 0 + block: + - name: SAP SWPM Pre Install - Detect - Stat directory {{ '/usr/sap/' ~ sap_swpm_sid ~ '/' }} + ansible.builtin.stat: + path: "{{ '/usr/sap/' ~ sap_swpm_sid ~ '' }}" + register: __sap_swpm_register_sapmnt_stat + + - name: SAP SWPM Pre Install - Detect - Find instance directories in {{ '/usr/sap/' ~ sap_swpm_sid ~ '/' }} + ansible.builtin.find: + paths: "/usr/sap/{{ sap_swpm_sid }}/" + file_type: directory + recurse: false + register: __sap_swpm_register_instance_dirs_find + when: __sap_swpm_register_sapmnt_stat.stat.exists + + # This task can take longer on installed instances. + # It took around 1 minute to get 14000 files in S4HANA 2025 PAS instance. + - name: SAP SWPM Pre Install - Detect - Check for files in matching directories + ansible.builtin.find: + paths: "{{ dir_item.path }}" + file_type: file + recurse: true + loop: "{{ __sap_swpm_register_instance_dirs_find.files | d([]) }}" + loop_control: + label: "{{ dir_item.path }}" + loop_var: dir_item + register: __sap_swpm_register_instance_dirs_content + when: dir_item.path | basename in __sap_swpm_detect_instance_names + + - name: SAP SWPM Pre Install - Detect - Display status of matching directories + ansible.builtin.debug: + msg: >- + Directory {{ file_item.dir_item.path }} contains {{ file_item.matched }} files. + loop: "{{ __sap_swpm_register_instance_dirs_content.results }}" + loop_control: + label: "{{ file_item.dir_item.path }}" + loop_var: file_item + when: + - file_item.skipped is not defined + - file_item.matched is defined + + - name: SAP SWPM Pre Install - Detect - Set fact if matching directories with files were found + ansible.builtin.set_fact: + __sap_swpm_fact_sap_directories_found: true + when: + - __sap_swpm_register_instance_dirs_content.results + | selectattr('matched', 'defined') + | selectattr('matched', 'gt', 0) + | list | length > 0 + + - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in directories + ansible.builtin.debug: + msg: | + WARN: Existing SAP instances detected in directories for {{ sap_swpm_sid }}: + {% for item in __sap_swpm_register_instance_dirs_content.results if item.matched is defined and item.matched > 0 %} + - Directory '{{ item.dir_item.path }}' contains {{ item.matched }} files. + {% endfor %} + when: __sap_swpm_fact_sap_directories_found | d(false) + + +## 4. Check profile files in /sapmnt//profile/ +- name: Block for profile files checks + when: __sap_swpm_detect_instance_names | d([]) | length > 0 + block: + - name: SAP SWPM Pre Install - Detect - Stat directory {{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }} + ansible.builtin.stat: + path: "{{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }}" + register: __sap_swpm_register_profile_dir_stat + + - name: SAP SWPM Pre Install - Detect - Find profile files in {{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }} + ansible.builtin.find: + paths: "/sapmnt/{{ sap_swpm_sid }}/profile/" + file_type: file + recurse: false + register: __sap_swpm_register_profiles_find + when: __sap_swpm_register_profile_dir_stat.stat.exists + + # Match pattern: __ (e.g., AE1_ASCS00_) + - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in profile files + ansible.builtin.set_fact: + __sap_swpm_fact_sap_profiles_found: >- + {%- set found = [] -%} + {%- for file in __sap_swpm_register_profiles_find.files | d([]) -%} + {%- for name in __sap_swpm_detect_instance_names -%} + {%- if (sap_swpm_sid ~ '_' ~ name ~ '_') in file.path | basename -%} + {%- set _ = found.append(file.path | basename) -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{ found | unique }} + + - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in profile files + ansible.builtin.debug: + msg: | + WARN: Existing SAP instances detected in profile files for {{ sap_swpm_sid }}: + {% for profile in __sap_swpm_fact_sap_profiles_found %} + - {{ profile }} + {% endfor %} + when: __sap_swpm_fact_sap_profiles_found | length > 0 + + +- name: SAP SWPM Pre Install - Detect - Set fact if clues to existing SAP instance were found + ansible.builtin.set_fact: + __sap_swpm_sap_detected: true + when: + - not sap_swpm_force | d(false) + - __sap_swpm_fact_sap_instances_found | d([]) | length > 0 + or __sap_swpm_fact_sap_services_found | d([]) | length > 0 + or __sap_swpm_fact_sap_profiles_found | d([]) | length > 0 + or __sap_swpm_fact_sap_directories_found | d(false) + +# This is very dynamic message that covers all possible scenarios. +- name: SAP SWPM Pre Install - Detect - Inform about next steps if SAP instance was detected + ansible.builtin.pause: + seconds: "{{ 30 if not sap_swpm_force | d(false) else 5 }}" + prompt: | + WARN: Clues to existing SAP instance(s) were detected for {{ sap_swpm_sid }}. + Product ID: '{{ sap_swpm_product_catalog_id | d('') }}' + + {% if sap_swpm_force | d(false) %} + SWPM installation steps will be executed regardless, + because 'sap_swpm_force' variable is set to 'true' (default is 'false'). + {% else %} + SWPM installation steps will be skipped to avoid potential issues with existing installation. + + If you intend to proceed with the installation despite this warning, + set 'sap_swpm_force' variable to 'true' and execute this Ansible Role again. + {% endif %} + + SAP Instances were detected based on the following checks: + {% if __sap_swpm_fact_sap_instances_found | d([]) | length > 0 %} + - Output of '/usr/sap/hostctrl/exe/hostctrl -function ListInstances'. + {% endif %} + {% if __sap_swpm_fact_sap_services_found | d([]) | length > 0 %} + - Contents of '/usr/sap/services' file. + {% endif %} + {% if __sap_swpm_fact_sap_profiles_found | d([]) | length > 0 %} + - Existence of profile files in '/sapmnt/{{ sap_swpm_sid }}/profile/' directory. + {% endif %} + {% if __sap_swpm_fact_sap_directories_found | d(false) %} + - Existence of files in '/usr/sap/{{ sap_swpm_sid }}/' directories. + {% endif %} + when: __sap_swpm_sap_detected | d(false) + + +# We cannot reliably detect some products, +# but we want to inform the user about that fact and potential risks of proceeding without detection. +- name: SAP SWPM Pre Install - Detect - Inform about products with limited detection capabilities + ansible.builtin.pause: + seconds: "{{ 30 if not sap_swpm_force | d(false) else 5 }}" + prompt: | + WARN: SAP Product {{ sap_swpm_product_catalog_id }} cannot be reliably detected by this role due to its nature. + + {% if __sap_swpm_fact_product_prefix in __sap_swpm_product_detection_skipped['database_load'] %} + - Database Load does not store any local files that can be detected. + SWPM handles repeated execution by itself, but you need to proceed with caution when targeting live SAP System. + {% endif %} + when: + - __sap_swpm_fact_product_prefix | d('') | trim | length > 0 + - __sap_swpm_fact_product_prefix + in __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten | unique diff --git a/roles/sap_swpm/tasks/pre_install/install_type.yml b/roles/sap_swpm/tasks/pre_install/install_type.yml index 284ecbfa..c3789d63 100644 --- a/roles/sap_swpm/tasks/pre_install/install_type.yml +++ b/roles/sap_swpm/tasks/pre_install/install_type.yml @@ -54,33 +54,19 @@ # Run Installation Type Steps ################ -- name: SAP SWPM Pre Install - Assert installation type is defined (including empty) - ansible.builtin.assert: - that: - - __sap_swpm_fact_installation_type is defined - fail_msg: - - "The installation type (variable '__sap_swpm_fact_installation_type') is not defined!" - success_msg: - - "The installation type (variable '__sap_swpm_fact_installation_type') is defined (including an empty string)." - - name: SAP SWPM Pre Install - Assert supported installation types ansible.builtin.assert: that: - - __sap_swpm_fact_installation_type == '' or - __sap_swpm_fact_installation_type == 'restore' or - __sap_swpm_fact_installation_type == 'ha' or - __sap_swpm_fact_installation_type == 'maint_plan_stack' or - __sap_swpm_fact_installation_type == 'ha_maint_plan_stack' - fail_msg: - - "The specified installation type, '{{ __sap_swpm_fact_installation_type }}', is not supported!" - - "The following installation types are supported:" - - "- '' (empty string, the default)" - - "- 'restore'" - - "- 'ha'" - - "- 'maint_plan_stack'" - - "- 'ha_maint_plan_stack'" - success_msg: - - "The specified installation type, '{{ __sap_swpm_fact_installation_type }}', is supported." + - __sap_swpm_fact_installation_type is string + - __sap_swpm_fact_installation_type in ['', 'restore', 'ha', 'maint_plan_stack', 'ha_maint_plan_stack'] + fail_msg: | + FAIL: Manual change of internal variable was detected! + The variable '__sap_swpm_fact_installation_type' allows only following options: + - '' (empty string, the default) + - 'restore' + - 'ha' + - 'maint_plan_stack' + - 'ha_maint_plan_stack' - name: SAP SWPM Pre Install - Assert that necessary variable for HA is defined ansible.builtin.assert: diff --git a/roles/sap_swpm/vars/main.yml b/roles/sap_swpm/vars/main.yml index b55cd647..4c9ade65 100644 --- a/roles/sap_swpm/vars/main.yml +++ b/roles/sap_swpm/vars/main.yml @@ -42,3 +42,105 @@ __sap_swpm_firewall_extra_packages: [] # Predefined firewall service names. __sap_swpm_firewall_service_name_cs: "sap-nw-central" __sap_swpm_firewall_service_name_app: "sap-nw-app" + + +### Internal variables for SAP detection. +# Boolean state of detection of SAP Instances. +__sap_swpm_sap_detected: false + +# List of SAP instance numbers generated in pre_install/detect_sap.yml. +__sap_swpm_detect_instance_numbers: [] + +# List of expected SAP instance names generated in pre_install/detect_sap.yml. +__sap_swpm_detect_instance_names: [] + +# Dictionary Matrix of product prefixes and sections combinations that must be enforced. +# This will ensure that required sections are present for specific products, +# while allowing other products to pass through and potentially fail during SWPM. +# NOTE: Dictionary contains variables that will be expanded only when accessed, which is after validation of inifile! +__sap_swpm_product_sections_dict: + # Product Catalog ID prefix. + # Example: 'NW_ABAP_ASCS' for product catalog ID 'NW_ABAP_ASCS:S4HANA2025.CORE.HDB.ABAP'. + # Contains: ASCS + NW_ABAP_ASCS: + # Name of section in 'sap_swpm_inifile_sections_list' that must be present for this product. + sections: + - nw_config_central_services_abap + # Instance name prefix used for detection of SAP instances. + instance_names: + - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr if sap_swpm_ascs_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_ascs_instance_nr | d('') }}" + + # Contains: PAS + NW_ABAP_CI: + sections: + - nw_config_primary_application_server_instance + instance_names: + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" + - "{{ 'D' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_pas_instance_nr | d('') }}" + + # Contains: ASCS, PAS (ABAP) + NW_ABAP_OneHost: + sections: + - nw_config_central_services_abap + - nw_config_primary_application_server_instance + instance_names: + - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr if sap_swpm_ascs_instance_nr | d('') | length > 0 else '' }}" + - "{{ 'D' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_ascs_instance_nr | d('') }}" + - "{{ sap_swpm_pas_instance_nr | d('') }}" + + # Contains: AAS (ABAP or JAVA) + NW_DI: + sections: + - nw_config_additional_application_server_instance + instance_names: + - "{{ 'D' ~ sap_swpm_aas_instance_nr if sap_swpm_aas_instance_nr | d('') | length > 0 else '' }}" + - "{{ 'J' ~ sap_swpm_aas_instance_nr if sap_swpm_aas_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_aas_instance_nr | d('') }}" + + # Contains: ERS + NW_ERS: + sections: + - nw_config_ers + instance_names: + - "{{ 'ERS' ~ sap_swpm_ers_instance_nr if sap_swpm_ers_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_ers_instance_nr | d('') }}" + + # Contains: ASCS, PAS (JAVA) + NW_Java_CI: + sections: + - nw_config_primary_application_server_instance + instance_names: + - "{{ 'J' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_pas_instance_nr | d('') }}" + + NW_Java_SCS: + sections: + - nw_config_central_services_java + instance_names: + - "{{ 'SCS' ~ sap_swpm_java_scs_instance_nr if sap_swpm_java_scs_instance_nr | d('') | length > 0 else '' }}" + instance_numbers: + - "{{ sap_swpm_java_scs_instance_nr | d('') }}" + + +# This is special list of product prefixes, where we cannot detect SAP, +# because it does not create any recognizable files. +# NOTE: Do not add product prefixes already in '__sap_swpm_product_sections_dict'! +__sap_swpm_product_detection_skipped: + # Product prefixes that are known for Database Load. + # Installs only 'saphostctrl' and rest is done in database. + # This is not conclusive enough for us to decide and SWPM handles this itself. + database_load: + - NW_ABAP_DB + - NW_Java_DB + # Space for more categories, if more products are found. Candidates: + # NW_ABAP_DB_DBRefresh, NW_Java_DB_DBRefresh, NW_Webdispatcher. From a5d1fe34e18417da9a95eeddc8d9b3176c75fc1c Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Mon, 11 May 2026 14:13:20 +0200 Subject: [PATCH 2/4] move detection into swpm_prepare and validate tags --- roles/sap_swpm/tasks/main.yml | 6 +- roles/sap_swpm/tasks/pre_install.yml | 52 ++------- .../sap_swpm/tasks/pre_install/detect_sap.yml | 31 +++++- .../tasks/pre_install/generate_inifile.yml | 40 +++++-- .../tasks/pre_install/swpm_prepare.yml | 103 ++++++++++-------- 5 files changed, 128 insertions(+), 104 deletions(-) diff --git a/roles/sap_swpm/tasks/main.yml b/roles/sap_swpm/tasks/main.yml index 2bf6fd3d..4e5d0585 100644 --- a/roles/sap_swpm/tasks/main.yml +++ b/roles/sap_swpm/tasks/main.yml @@ -40,14 +40,14 @@ tags: always - name: SAP SWPM - Run pre_install tasks - ansible.builtin.include_tasks: + ansible.builtin.import_tasks: file: pre_install.yml tags: - sap_swpm_pre_install - sap_swpm_install - name: SAP SWPM - Run swpm - ansible.builtin.include_tasks: + ansible.builtin.import_tasks: file: swpm.yml when: - sap_swpm_run_sapinst @@ -56,6 +56,6 @@ - sap_swpm_install - name: SAP SWPM - Run post_install task - ansible.builtin.include_tasks: + ansible.builtin.import_tasks: file: post_install.yml when: sap_swpm_run_sapinst diff --git a/roles/sap_swpm/tasks/pre_install.yml b/roles/sap_swpm/tasks/pre_install.yml index 28da820e..1f79df28 100644 --- a/roles/sap_swpm/tasks/pre_install.yml +++ b/roles/sap_swpm/tasks/pre_install.yml @@ -289,55 +289,18 @@ tags: sap_swpm_update_etchosts -- name: SAP SWPM Pre Install - Set fact with Product ID prefix - ansible.builtin.set_fact: - __sap_swpm_fact_product_prefix: "{{ sap_swpm_product_catalog_id.split(':') | first }}" - when: - - sap_swpm_product_catalog_id | d('') | trim | length > 0 - - "':' in sap_swpm_product_catalog_id" - -# While SWPM allows providing extra inputs and ignores them, -# we want to ensure that some sections are provided before failure occurs inside of SWPM execution. -- name: SAP SWPM Pre Install - Assert that inifile sections are provided for product - ansible.builtin.assert: - that: >- - (__sap_swpm_product_sections_dict[__product_prefix]['sections'] - | intersect(sap_swpm_inifile_sections_list) | length) - == __sap_swpm_product_sections_dict[__product_prefix]['sections'] | length - fail_msg: | - FAIL: Product '{{ sap_swpm_product_catalog_id }}' requires - all of the following sections to be defined in 'sap_swpm_inifile_sections_list': - {{ __sap_swpm_product_sections_dict[__product_prefix]['sections'] | join(', ') }}. - vars: - __product_prefix: "{{ sap_swpm_product_catalog_id.split(':') | first }}" - when: - - sap_swpm_inifile_sections_list | d([]) | length > 0 - - sap_swpm_product_catalog_id | d('') | trim | length > 0 - - "':' in sap_swpm_product_catalog_id" - - __sap_swpm_product_sections_dict[__product_prefix] is defined - -# Detect running SAP instances using variables validated in tasks above. -# NOTE: This will result in skipping of remaining tasks, unless force is used. -- name: SAP SWPM Pre Install - Detect running SAP instances - ansible.builtin.include_tasks: - file: pre_install/detect_sap.yml - apply: - tags: always - tags: always - +# Run Pre-installation Steps Based on Run Mode +# NOTE: This task file includes SAP detection logic, which will be skipped for these tags. +- name: SAP SWPM Pre Install - Run Pre-installation Steps + ansible.builtin.include_tasks: pre_install/swpm_prepare.yml + tags: + - sap_swpm_generate_inifile + - sap_swpm_sapinst_commandline - name: Block to execute on empty host or in force mode when: not __sap_swpm_sap_detected | d(false) or sap_swpm_force | d(false) block: - # Run Pre-installation Steps Based on Run Mode - - name: SAP SWPM Pre Install - Run Pre-installation Steps - ansible.builtin.include_tasks: pre_install/swpm_prepare.yml - tags: - - sap_swpm_generate_inifile - - sap_swpm_sapinst_commandline - - ## Pre Install Optional Tasks # Firewall - name: SAP SWPM Pre Install - Configure Firewall @@ -350,7 +313,6 @@ tags: sap_swpm_configure_firewall # /etc/hosts - - name: SAP SWPM Pre Install - Update /etc/hosts ansible.builtin.include_tasks: file: pre_install/update_etchosts.yml diff --git a/roles/sap_swpm/tasks/pre_install/detect_sap.yml b/roles/sap_swpm/tasks/pre_install/detect_sap.yml index 1b7367d4..825e1f99 100644 --- a/roles/sap_swpm/tasks/pre_install/detect_sap.yml +++ b/roles/sap_swpm/tasks/pre_install/detect_sap.yml @@ -15,7 +15,26 @@ # 6. Inform user about detected instances and potential risks of proceeding with installation. -## 0. Prepare lists of instance names and numbers for detection. +# Extract prefix from Product ID, or inform that prefix is not recognized. +- name: SAP SWPM Pre Install - Set fact with SAP Product ID prefix + ansible.builtin.set_fact: + __sap_swpm_fact_product_prefix: >- + {{ sap_swpm_product_catalog_id.split(':') | first + if sap_swpm_product_catalog_id | d('') | trim | length > 0 and ':' in sap_swpm_product_catalog_id + else '' }} + +- name: SAP SWPM Pre Install - Detect - Inform if SAP Product ID prefix is unavailable + ansible.builtin.debug: + msg: | + WARN: Unable to extract SAP Product ID prefix for product catalog ID '{{ sap_swpm_product_catalog_id }}'. + Expected format of product catalog ID is 'PREFIX:REST_OF_ID' (e.g., 'NW_ABAP_ASCS:S4HANA2025.CORE.HDB.ABAP'). + Detection of existing SAP instances will proceed based on contents of 'sap_swpm_inifile_sections_list' variable. + + NOTE: Providing invalid SAP Product ID will most likely lead into failure during SWPM execution. + when: __sap_swpm_fact_product_prefix | length == 0 + + +# Prepare lists of instance names and numbers for detection. # Lists based on Product ID. # Regex is used to remove items with empty values. - name: SAP SWPM Pre Install - Detect - Set fact with lists for detection using product ID @@ -27,7 +46,7 @@ "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instance_names'] | select('regex', '.+') | unique | list }}" when: - - __sap_swpm_fact_product_prefix | d('') | trim | length > 0 + - __sap_swpm_fact_product_prefix | trim | length > 0 - __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix] is defined @@ -37,10 +56,10 @@ # Falling back to Ansible unique filter as Jinja2 one failed: 'sap_swpm_ascs_instance_nr' is undefined - name: Block to fall back on sections if product ID is not in dictionary when: - - __sap_swpm_fact_product_prefix | d('') | trim | length == 0 - or __sap_swpm_fact_product_prefix not in __sap_swpm_product_sections_dict - - __sap_swpm_fact_product_prefix - not in __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten | unique + - __sap_swpm_fact_product_prefix | trim | length == 0 + or __sap_swpm_fact_product_prefix not in ( + __sap_swpm_product_sections_dict.keys() | list + + __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten) block: - name: SAP SWPM Pre Install - Detect - Set fact with lists for ASCS detection diff --git a/roles/sap_swpm/tasks/pre_install/generate_inifile.yml b/roles/sap_swpm/tasks/pre_install/generate_inifile.yml index 5bbc20c9..54965e94 100644 --- a/roles/sap_swpm/tasks/pre_install/generate_inifile.yml +++ b/roles/sap_swpm/tasks/pre_install/generate_inifile.yml @@ -38,7 +38,7 @@ # At this point, the following variables need to be set: # sap_swpm_product_catalog_id, __sap_swpm_fact_software_path, sap_swpm_sid, sap_swpm_fqdn -- name: SAP SWPM Pre Install - Assert that certain variables are set +- name: SAP SWPM Pre Install - Assert that required variables are set correctly ansible.builtin.include_tasks: file: assert_vars_length_loop_block.yml apply: @@ -52,14 +52,40 @@ loop_var: __sap_swpm_vars_to_be_asserted_for_length_line_item tags: sap_swpm_generate_inifile -- name: SAP SWPM Pre Install - Display these variables +- name: SAP SWPM Pre Install - Display contents of required variables ansible.builtin.debug: msg: | - "sap_swpm_product_catalog_id: '{{ sap_swpm_product_catalog_id }}'" - "sap_swpm_sid: '{{ sap_swpm_sid }}'" - "sap_swpm_fqdn: '{{ sap_swpm_fqdn }}'" - " " - "The SAP software is taken from directory: '{{ __sap_swpm_fact_software_path }}'" + sap_swpm_product_catalog_id: '{{ sap_swpm_product_catalog_id }}' + sap_swpm_sid: '{{ sap_swpm_sid }}' + sap_swpm_fqdn: '{{ sap_swpm_fqdn }}' + + The SAP software is taken from directory: '{{ __sap_swpm_fact_software_path }}' + + +# While SWPM allows providing extra inputs and ignores them, +# we want to ensure that some sections are provided before failure occurs inside of SWPM execution. +- name: SAP SWPM Pre Install - Assert that inifile sections are provided for product + ansible.builtin.assert: + that: >- + (__sap_swpm_product_sections_dict[__product_prefix]['sections'] + | intersect(sap_swpm_inifile_sections_list) | length) + == __sap_swpm_product_sections_dict[__product_prefix]['sections'] | length + fail_msg: | + FAIL: The SAP Product '{{ sap_swpm_product_catalog_id }}' + requires all of the following sections to be defined + in the variable 'sap_swpm_inifile_sections_list': + {% for section in __sap_swpm_product_sections_dict[__product_prefix]['sections'] | d([]) %} + - {{ section }} + {% endfor %} + vars: + __product_prefix: "{{ sap_swpm_product_catalog_id.split(':') | first }}" + when: + - sap_swpm_inifile_sections_list | d([]) | length > 0 + - sap_swpm_product_catalog_id | d('') | trim | length > 0 + - "':' in sap_swpm_product_catalog_id" + - __sap_swpm_product_sections_dict[__product_prefix] is defined + tags: sap_swpm_generate_inifile + # We are creating the inifile dynamically in one of the two cases: # 1 - The tag sap_swpm_generate_inifile is specified diff --git a/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml b/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml index 018b4b0e..15857ece 100644 --- a/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml +++ b/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml @@ -24,52 +24,69 @@ ansible.builtin.include_tasks: "generate_inifile.yml" tags: sap_swpm_generate_inifile -# Determine Installation Type, e.g. System Copy, HA, Maintenance Planner, ... -- name: SAP SWPM default mode - Determine Installation Type + +# Detect running SAP instances using variables validated in tasks above. +# NOTE: This will result in skipping of remaining tasks, unless force is used. +- name: SAP SWPM Pre Install - Detect running SAP instances ansible.builtin.include_tasks: - file: install_type.yml -# apply: -# tags: sap_swpm_generate_inifile -# tags: sap_swpm_generate_inifile + file: pre_install/detect_sap.yml + # Inherited tags are explicitly declared here to ensure this inclusion + # is skipped (and logs are hidden) when only sub-tags like 'sap_swpm_generate_inifile' are requested. + tags: + - sap_swpm_pre_install + - sap_swpm_install -# Requires variables - sap_swpm_software_path (e.g. /software/download_basket), __sap_swpm_fact_sapcar_path (e.g. /software/sapcar), __sap_swpm_fact_swpm_path (e.g. /software/swpm) -# Prepare Software -- name: SAP SWPM Pre Install - Prepare Software - ansible.builtin.include_tasks: prepare_software.yml - when: sap_swpm_run_sapinst -# Set fact for SWPM path -- name: SAP SWPM Pre Install - Set fact for SWPM path when extract directory defined - ansible.builtin.set_fact: - __sap_swpm_fact_sapinst_path: "{{ sap_swpm_software_extract_directory }}" - when: - - sap_swpm_run_sapinst - - sap_swpm_software_extract_directory is defined - - not (sap_swpm_software_extract_directory is none or (sap_swpm_software_extract_directory | length == 0)) +- name: Block to execute only if SAP instance is not detected or in force mode. + when: not __sap_swpm_sap_detected | d(false) or sap_swpm_force | d(false) + block: -# Set fact for SWPM path -- name: SAP SWPM Pre Install - Set fact for SWPM path when undefined extract directory - ansible.builtin.set_fact: - __sap_swpm_fact_sapinst_path: "{{ (__sap_swpm_fact_swpm_path | regex_replace('\\/$', '')) + '/extracted' }}" - when: - - sap_swpm_run_sapinst - - sap_swpm_software_extract_directory is undefined or (sap_swpm_software_extract_directory is none or (sap_swpm_software_extract_directory | length) == 0) + # Determine Installation Type, e.g. System Copy, HA, Maintenance Planner, ... + - name: SAP SWPM default mode - Determine Installation Type + ansible.builtin.include_tasks: + file: install_type.yml + # apply: + # tags: sap_swpm_generate_inifile + # tags: sap_swpm_generate_inifile -- name: SAP SWPM Pre Install - Ensure directory '{{ __sap_swpm_fact_sapinst_path }}' exists - ansible.builtin.file: - path: "{{ __sap_swpm_fact_sapinst_path }}" - state: directory - mode: '0755' - when: sap_swpm_run_sapinst + # Requires variables - sap_swpm_software_path (e.g. /software/download_basket), __sap_swpm_fact_sapcar_path (e.g. /software/sapcar), __sap_swpm_fact_swpm_path (e.g. /software/swpm) + # Prepare Software + - name: SAP SWPM Pre Install - Prepare Software + ansible.builtin.include_tasks: prepare_software.yml + when: sap_swpm_run_sapinst + + # Set fact for SWPM path + - name: SAP SWPM Pre Install - Set fact for SWPM path when extract directory defined + ansible.builtin.set_fact: + __sap_swpm_fact_sapinst_path: "{{ sap_swpm_software_extract_directory }}" + when: + - sap_swpm_run_sapinst + - sap_swpm_software_extract_directory is defined + - not (sap_swpm_software_extract_directory is none or (sap_swpm_software_extract_directory | length == 0)) + + # Set fact for SWPM path + - name: SAP SWPM Pre Install - Set fact for SWPM path when undefined extract directory + ansible.builtin.set_fact: + __sap_swpm_fact_sapinst_path: "{{ (__sap_swpm_fact_swpm_path | regex_replace('\\/$', '')) + '/extracted' }}" + when: + - sap_swpm_run_sapinst + - sap_swpm_software_extract_directory is undefined or (sap_swpm_software_extract_directory is none or (sap_swpm_software_extract_directory | length) == 0) + + - name: SAP SWPM Pre Install - Ensure directory '{{ __sap_swpm_fact_sapinst_path }}' exists + ansible.builtin.file: + path: "{{ __sap_swpm_fact_sapinst_path }}" + state: directory + mode: '0755' + when: sap_swpm_run_sapinst -# Extract SWPM -- name: SAP SWPM Pre Install - Extract SWPM - ansible.builtin.command: >- - {{ __sap_swpm_fact_sapcar_path }}/{{ __sap_swpm_fact_sapcar_file_name }} \ - -xvf {{ __sap_swpm_fact_swpm_path }}/{{ __sap_swpm_fact_swpm_file_name }} \ - -manifest SIGNATURE.SMF - register: sap_swpm_extractswpm - args: - chdir: "{{ __sap_swpm_fact_sapinst_path }}" - changed_when: "'SAPCAR: processing archive' in sap_swpm_extractswpm.stdout" - when: sap_swpm_run_sapinst + # Extract SWPM + - name: SAP SWPM Pre Install - Extract SWPM + ansible.builtin.command: >- + {{ __sap_swpm_fact_sapcar_path }}/{{ __sap_swpm_fact_sapcar_file_name }} \ + -xvf {{ __sap_swpm_fact_swpm_path }}/{{ __sap_swpm_fact_swpm_file_name }} \ + -manifest SIGNATURE.SMF + register: sap_swpm_extractswpm + args: + chdir: "{{ __sap_swpm_fact_sapinst_path }}" + changed_when: "'SAPCAR: processing archive' in sap_swpm_extractswpm.stdout" + when: sap_swpm_run_sapinst From aa2a47a37e18b4161d45f1fdf12a0ab1e89e8cfd Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Mon, 11 May 2026 14:30:46 +0200 Subject: [PATCH 3/4] update readme with sap_swpm_force var --- roles/sap_swpm/README.md | 7 +++++++ roles/sap_swpm/defaults/main.yml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/roles/sap_swpm/README.md b/roles/sap_swpm/README.md index a1140316..b14492d7 100644 --- a/roles/sap_swpm/README.md +++ b/roles/sap_swpm/README.md @@ -685,6 +685,13 @@ Define UID of Linux user SIDADM. ### Miscellaneous Variables +#### sap_swpm_force +- _Type:_ `bool` +- _Default:_ `false` + +Optional override that forces execution of installation tasks if existing SAP instance is detected.
+This variable should be used with caution as running SWPM installation with active SAP instances can lead to corruption of existing SAP installation. + #### sap_swpm_ascs_install_gateway - _Type:_ `string` - _Default:_ `true` diff --git a/roles/sap_swpm/defaults/main.yml b/roles/sap_swpm/defaults/main.yml index 5224e715..9971915a 100644 --- a/roles/sap_swpm/defaults/main.yml +++ b/roles/sap_swpm/defaults/main.yml @@ -509,6 +509,6 @@ sap_swpm_display_unattended_output: false # Set which Ansible Collection to use when calling sap_install roles. sap_swpm_sap_install_collection: 'community.sap_install' -# Optional override that avoids failures if existing SAP instance is detected. +# Optional override that forces execution of installation tasks if existing SAP instance is detected. # This variable should be used with caution as running SWPM installation with active SAP instances can lead to corruption of existing SAP installation. sap_swpm_force: false From 06e15bde59328e683dfba96b4f7a1d084f73f262 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Wed, 20 May 2026 16:43:09 +0200 Subject: [PATCH 4/4] replace relaxed detection with very strict detection to improve reliability --- roles/sap_swpm/defaults/main.yml | 8 +- .../sap_swpm/tasks/pre_install/detect_sap.yml | 651 +++++++++--------- .../tasks/pre_install/detect_sap_instance.yml | 232 +++++++ .../tasks/pre_install/detect_sap_prepare.yml | 119 ++++ .../tasks/pre_install/swpm_prepare.yml | 27 +- roles/sap_swpm/vars/main.yml | 113 +-- 6 files changed, 770 insertions(+), 380 deletions(-) create mode 100644 roles/sap_swpm/tasks/pre_install/detect_sap_instance.yml create mode 100644 roles/sap_swpm/tasks/pre_install/detect_sap_prepare.yml diff --git a/roles/sap_swpm/defaults/main.yml b/roles/sap_swpm/defaults/main.yml index 9971915a..b9ded361 100644 --- a/roles/sap_swpm/defaults/main.yml +++ b/roles/sap_swpm/defaults/main.yml @@ -510,5 +510,11 @@ sap_swpm_display_unattended_output: false sap_swpm_sap_install_collection: 'community.sap_install' # Optional override that forces execution of installation tasks if existing SAP instance is detected. -# This variable should be used with caution as running SWPM installation with active SAP instances can lead to corruption of existing SAP installation. +# WARNING: This variable should be used with caution +# as running SWPM installation with active SAP instances can lead to corruption of existing SAP installation. sap_swpm_force: false + +# Skip all detection checks. Use ONLY for testing/development on clean systems. +# WARNING: Skipping detection can lead to data loss if installation already exists! +# This will bypass all safety checks including saphostctrl, sapcontrol, and file existence checks. +# sap_swpm_skip_detection: false diff --git a/roles/sap_swpm/tasks/pre_install/detect_sap.yml b/roles/sap_swpm/tasks/pre_install/detect_sap.yml index 825e1f99..446a3c46 100644 --- a/roles/sap_swpm/tasks/pre_install/detect_sap.yml +++ b/roles/sap_swpm/tasks/pre_install/detect_sap.yml @@ -1,355 +1,358 @@ # SPDX-License-Identifier: Apache-2.0 --- -# This is pseudo-idempotency validation to ensure that remaining SWPM tasks are skipped if there are running SAP instances detected. -# This is to prevent any potential issues that could arise from running the SWPM installation while SAP instances are active. +### List of scenarios for determining next steps. +# This ensures that installation can be skipped when needed to prevent any potential issues affecting existing installations. # Optional override is available in defaults/main.yml to execute regardless of warning. +# NOTE: SAP Host Agent can be skipped during installation using 'sap_swpm_install_saphostagent: false'. + +## 'saphostctrl' is present: +# 1. SKIP - 'root' saphostctrl check finds the desired SID and instance number. +# 2. INSTALL - 'root' saphostctrl check does not find the desired SID and instance number, and no other indicators are found. +# 3. FAIL - 'root' saphostctrl check does not find the desired SID and instance number, and any other indicators are found. + +# 'saphostctrl' is not present: +# 4. SKIP - 'sidadm' sapcontrol check finds the desired SID and instance number. +# 5. INSTALL - 'sidadm' sapcontrol check does not find the desired SID and instance number, and no other indicators are found. +# 6. FAIL - 'sidadm' sapcontrol check does not find the desired SID and instance number, and any other indicators are found. + +# Breakdown of potential scenarios and actions: +# | saphostctrl | sapcontrol | Instance Found | Files Found | Action +# |-------------|------------|----------------|-------------|-------- +# | Yes | N/A | Yes | N/A | SKIP +# | Yes | N/A | No | No | INSTALL +# | Yes | N/A | No | Any | FAIL +# | No | Yes | Yes | N/A | SKIP +# | No | Yes | No | Any | FAIL +# | No | No | No | No | INSTALL + +# List of non-saphostctrl indicators for potential existing SAP installation: +# A. Files found in /usr/sap/ directory. +# B. Profile files found in /sapmnt//profile/ directory. +# C. Services found in /usr/sap/sapservices file. + + +# Check 1: Check if the group 'sapsys' exists with correct ID. +# The role supports specifying the 'sapsys' group id in variable `sap_swpm_sapsys_gid`. +# The installation will fail if there is already a group named sapsys but with a different ID. +# NOTE: This is executed even if the above checks detect an existing installation, because we want to identify conflicts. + +# This is also used for validation before using 'sidadm' sapcontrol. +- name: SAP SWPM Pre Install - Detect - Get details of the 'sapsys' group + ansible.builtin.getent: + database: group + key: sapsys + # Hide output if group does not exist. + failed_when: false + +# getent_group will be populated, with fields: +# [0] - 'X' if the password is set. +# [1] - Group ID. +# [2] - Group members. +- name: SAP SWPM Pre Install - Detect - Fail if conflict exists for group ID 'sapsys' + ansible.builtin.fail: + msg: | + FAIL: Group 'sapsys' exists but with a different group ID. + Expected: '{{ sap_swpm_sapsys_gid }}' + Found: '{{ ansible_facts["getent_group"]['sapsys'][1] }}' + when: + - sap_swpm_sapsys_gid is defined + - sap_swpm_sapsys_gid is string + - sap_swpm_sapsys_gid | trim | length > 0 + # Actual comparison of Group ID + - ansible_facts["getent_group"]['sapsys'][1] is defined + - ansible_facts["getent_group"]['sapsys'][1] | int != sap_swpm_sapsys_gid | int + + +# Check 2: Check if the user 'sidadm' exists with correct ID. +# The role supports specifying the 'sidadm' user id in variable `sap_swpm_sidadm_uid`. +# The installation will fail if there is already a user named sidadm but with a different ID. +# NOTE: This is executed even if above check detected existing installation, because we want to know of conflict. + +# This is also used for validation before using 'sidadm' sapcontrol. +- name: SAP SWPM Pre Install - Detect - Get details about available users + ansible.builtin.getent: + database: passwd + # Hide output if user does not exist. + failed_when: false + +# getent_passwd will be populated, with fields: +# [0] - 'X' if the password is set. +# [1] - User ID. +# [2] - Group ID. +- name: SAP SWPM Pre Install - Detect - Fail if conflict exists for user ID '{{ __sap_swpm_sidadm_user }}' + ansible.builtin.fail: + msg: | + FAIL: User '{{ __sap_swpm_sidadm_user }}' exists but with a different user ID. + Expected: '{{ sap_swpm_sidadm_uid }}' + Found: '{{ ansible_facts["getent_passwd"][__sap_swpm_sidadm_user][1] }}' + when: + - sap_swpm_sidadm_uid is defined + - sap_swpm_sidadm_uid is string + - sap_swpm_sidadm_uid | trim | length > 0 + # Actual comparison of User ID + - ansible_facts["getent_passwd"][__sap_swpm_sidadm_user][1] is defined + - ansible_facts["getent_passwd"][__sap_swpm_sidadm_user][1] | int != sap_swpm_sidadm_uid | int + + +# Check 3: Check if the user 'sapadm' exists with correct ID. +# The role supports specifying the 'sapadm' user id in variable `sap_swpm_sapadm_uid`. +# The installation will fail if there is already a user named sapadm but with a different ID. +# NOTE: This is executed even if above check detected existing installation, because we want to know of conflict. + +# The getent data for 'sapadm' is collected in the previous check. +# Running getent twice with different key filters would prevent checking both users in subsequent tasks. + +# getent_passwd will be populated, with fields: +# [0] - 'X' if the password is set. +# [1] - User ID. +# [2] - Group ID. +- name: SAP SWPM Pre Install - Detect - Fail if conflict exists for user ID 'sapadm' + ansible.builtin.fail: + msg: | + FAIL: User 'sapadm' exists but with a different user ID. + Expected: '{{ sap_swpm_sapadm_uid }}' + Found: '{{ ansible_facts["getent_passwd"]['sapadm'][1] }}' + when: + - sap_swpm_sapadm_uid is defined + - sap_swpm_sapadm_uid is string + - sap_swpm_sapadm_uid | trim | length > 0 + # Actual comparison of User ID + - ansible_facts["getent_passwd"]['sapadm'][1] is defined + - ansible_facts["getent_passwd"]['sapadm'][1] | int != sap_swpm_sapadm_uid | int + + +### Execute global data gathering before per-instance detection. + +# Check 4: Detect 'saphostctrl' and search for SID and instance number in 'ListInstances' output. +- name: SAP SWPM Pre Install - Detect - Stat /usr/sap/hostctrl/exe/saphostctrl file + ansible.builtin.stat: + path: /usr/sap/hostctrl/exe/saphostctrl + register: __sap_swpm_register_saphostctrl_stat + +# Output example: +# Inst Info : AE1 - 00 - ae1ascs-ha - 793, patch 51, commit 375ae433dffb023985041c1d7b4687b1bde8d211 +# Inst Info : AE1 - 10 - ae1ers-ha - 793, patch 51, commit 375ae433dffb023985041c1d7b4687b1bde8d211 +- name: SAP SWPM Pre Install - Detect - Execute saphostctrl function ListInstances + ansible.builtin.command: + cmd: /usr/sap/hostctrl/exe/saphostctrl -function ListInstances + register: __sap_swpm_register_saphostctrl_list_instances + changed_when: false + # Hides failed output, but 'rc' can be still used for verification. + failed_when: false + when: + - __sap_swpm_register_saphostctrl_stat.stat.exists + - __sap_swpm_register_saphostctrl_stat.stat.executable -# Steps for checking existing SAP instances: -# 0. Prepare lists of instance names and numbers for detection based on provided variables and product catalog ID. -# 1. Check output from /usr/sap/hostctrl/exe/saphostctrl -function ListInstances -# 2. Check contents of /usr/sap/sapservices -# 3. Check contents of /usr/sap// directories -# 4. Check profile files in /sapmnt//profile/ -# 5. Set boolean flag for skipping further tasks. -# 6. Inform user about detected instances and potential risks of proceeding with installation. - - -# Extract prefix from Product ID, or inform that prefix is not recognized. -- name: SAP SWPM Pre Install - Set fact with SAP Product ID prefix - ansible.builtin.set_fact: - __sap_swpm_fact_product_prefix: >- - {{ sap_swpm_product_catalog_id.split(':') | first - if sap_swpm_product_catalog_id | d('') | trim | length > 0 and ':' in sap_swpm_product_catalog_id - else '' }} - -- name: SAP SWPM Pre Install - Detect - Inform if SAP Product ID prefix is unavailable +- name: SAP SWPM Pre Install - Detect - Show reason why saphostctrl check was skipped ansible.builtin.debug: msg: | - WARN: Unable to extract SAP Product ID prefix for product catalog ID '{{ sap_swpm_product_catalog_id }}'. - Expected format of product catalog ID is 'PREFIX:REST_OF_ID' (e.g., 'NW_ABAP_ASCS:S4HANA2025.CORE.HDB.ABAP'). - Detection of existing SAP instances will proceed based on contents of 'sap_swpm_inifile_sections_list' variable. - - NOTE: Providing invalid SAP Product ID will most likely lead into failure during SWPM execution. - when: __sap_swpm_fact_product_prefix | length == 0 - - -# Prepare lists of instance names and numbers for detection. -# Lists based on Product ID. -# Regex is used to remove items with empty values. -- name: SAP SWPM Pre Install - Detect - Set fact with lists for detection using product ID - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: - "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instance_numbers'] - | select('regex', '.+') | unique | list }}" - __sap_swpm_detect_instance_names: - "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instance_names'] - | select('regex', '.+') | unique | list }}" + INFO: Detection of existing SAP instance installation with 'saphostctrl' command was skipped. + {% if not __sap_swpm_register_saphostctrl_stat.stat.exists %} + The executable '/usr/sap/hostctrl/exe/saphostctrl' is not available. + {% elif not __sap_swpm_register_saphostctrl_stat.stat.executable %} + The executable '/usr/sap/hostctrl/exe/saphostctrl' is not executable. + {% endif %} + {% if __sap_swpm_register_saphostctrl_list_instances.rc is defined + and __sap_swpm_register_saphostctrl_list_instances.rc != 0 %} + Return code: {{ __sap_swpm_register_saphostctrl_list_instances.rc | d('1') }} + Output: {{ __sap_swpm_register_saphostctrl_list_instances.stdout | d('') }} + Error: {{ __sap_swpm_register_saphostctrl_list_instances.stderr | d('') }} + {% endif %} when: - - __sap_swpm_fact_product_prefix | trim | length > 0 - - __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix] is defined + - not __sap_swpm_register_saphostctrl_stat.stat.exists + or not __sap_swpm_register_saphostctrl_stat.stat.executable + or (__sap_swpm_register_saphostctrl_list_instances.rc is defined + and __sap_swpm_register_saphostctrl_list_instances.rc != 0) + +- name: SAP SWPM Pre Install - Detect - Show saphostctrl command output in verbose mode + ansible.builtin.debug: + var: __sap_swpm_register_saphostctrl_list_instances + when: ansible_verbosity >= 2 -# Lists based on sections list if Product ID cannot be used. -# NOTE: Defaults before 'unique' do not serve any purpose, because when already validates it, -# but it is required for linting to pass: -# Falling back to Ansible unique filter as Jinja2 one failed: 'sap_swpm_ascs_instance_nr' is undefined -- name: Block to fall back on sections if product ID is not in dictionary +# Check 5: Use 'sapcontrol' and search for SID and instance number in 'GetInstanceProperties' output. +# The `sapcontrol` command from SAP kernel files can be used to obtain instance information. +- name: Block for sapcontrol checks when: - - __sap_swpm_fact_product_prefix | trim | length == 0 - or __sap_swpm_fact_product_prefix not in ( - __sap_swpm_product_sections_dict.keys() | list - + __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten) + - ansible_facts["getent_group"]['sapsys'][1] is defined + - ansible_facts["getent_passwd"][__sap_swpm_sidadm_user][1] is defined block: - - name: SAP SWPM Pre Install - Detect - Set fact with lists for ASCS detection - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: >- - {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_ascs_instance_nr | d('')]) | unique }} - __sap_swpm_detect_instance_names: >- - {{ (__sap_swpm_detect_instance_names | d([]) + ['ASCS' ~ sap_swpm_ascs_instance_nr]) | d('') | unique }} - when: - - sap_swpm_ascs_instance_nr | d('') | length > 0 - - "'nw_config_central_services_abap' in sap_swpm_inifile_sections_list" - - - name: SAP SWPM Pre Install - Detect - Set fact with lists for SCS detection - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: >- - {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_java_scs_instance_nr | d('')]) | unique }} - __sap_swpm_detect_instance_names: >- - {{ (__sap_swpm_detect_instance_names | d([]) + ['SCS' ~ sap_swpm_java_scs_instance_nr]) | unique }} - when: - - sap_swpm_java_scs_instance_nr | d('') | length > 0 - - "'nw_config_central_services_java' in sap_swpm_inifile_sections_list" - - - name: SAP SWPM Pre Install - Detect - Set fact with lists for ERS detection - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: >- - {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_ers_instance_nr | d('')]) | unique }} - __sap_swpm_detect_instance_names: >- - {{ (__sap_swpm_detect_instance_names | d([]) + ['ERS' ~ sap_swpm_ers_instance_nr]) | unique }} - when: - - sap_swpm_ers_instance_nr | d('') | length > 0 - - "'nw_config_ers' in sap_swpm_inifile_sections_list" - - - name: SAP SWPM Pre Install - Detect - Set fact with lists for PAS detection - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: >- - {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_pas_instance_nr | d('')]) | unique }} - __sap_swpm_detect_instance_names: >- - {{ (__sap_swpm_detect_instance_names | d([]) + __pas_instance_names) | unique }} - vars: - __pas_instance_names: - - "{{ 'D' ~ sap_swpm_pas_instance_nr }}" - - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr }}" - - "{{ 'J' ~ sap_swpm_pas_instance_nr }}" - when: - - sap_swpm_pas_instance_nr | d('') | length > 0 - - "'nw_config_primary_application_server_instance' in sap_swpm_inifile_sections_list" - - - name: SAP SWPM Pre Install - Detect - Set fact with lists for AAS detection - ansible.builtin.set_fact: - __sap_swpm_detect_instance_numbers: >- - {{ (__sap_swpm_detect_instance_numbers | d([]) + [sap_swpm_aas_instance_nr | d('')]) | unique }} - __sap_swpm_detect_instance_names: >- - {{ (__sap_swpm_detect_instance_names | d([]) + __aas_instance_names) | unique }} - vars: - __aas_instance_names: - - "{{ 'D' ~ sap_swpm_aas_instance_nr }}" - - "{{ 'J' ~ sap_swpm_aas_instance_nr }}" - when: - - sap_swpm_aas_instance_nr | d('') | length > 0 - - "'nw_config_additional_application_server_instance' in sap_swpm_inifile_sections_list" - - -## 1. Check output from /usr/sap/hostctrl/exe/saphostctrl -function ListInstances -- name: Block for saphostctrl checks - when: __sap_swpm_detect_instance_numbers | d([]) | length > 0 - block: - - name: SAP SWPM Pre Install - Detect - Stat /usr/sap/hostctrl/exe/saphostctrl file - ansible.builtin.stat: - path: /usr/sap/hostctrl/exe/saphostctrl - register: __sap_swpm_register_saphostctrl_stat - - - name: SAP SWPM Pre Install - Detect - Execute saphostctrl function ListInstances - ansible.builtin.command: - cmd: /usr/sap/hostctrl/exe/saphostctrl -function ListInstances - register: __sap_swpm_register_saphostctrl_listinstances - ignore_errors: true + # Reason for noqa: Switch to user requires shell module to load environment variables. + - name: SAP SWPM Pre Install - Detect - Check if 'sapcontrol' command is available # noqa: command-instead-of-shell + ansible.builtin.shell: + cmd: "bash -lc 'which sapcontrol'" + become: true + become_user: "{{ __sap_swpm_sidadm_user }}" + register: __sap_swpm_register_which_sapcontrol changed_when: false - when: - - __sap_swpm_register_saphostctrl_stat.stat.exists - - __sap_swpm_register_saphostctrl_stat.stat.executable - - # Match pattern: " - ". - # Example: Inst Info : AE1 - 10 - ae1ers-ha - 793, patch 51, commit 375ae433dffb023985041c1d7b4687b1bde8d211 - - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in saphostctrl output - ansible.builtin.set_fact: - __sap_swpm_fact_sap_instances_found: >- - {%- set found = [] -%} - {%- for line in __sap_swpm_register_saphostctrl_listinstances.stdout_lines | d([]) -%} - {%- for nr in __sap_swpm_detect_instance_numbers -%} - {%- if line is search(' ' ~ sap_swpm_sid ~ ' - ' ~ nr ~ ' ') -%} - {%- set _ = found.append(nr) -%} - {%- endif -%} - {%- endfor -%} - {%- endfor -%} - {{ found | unique }} - - - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in saphostctrl - ansible.builtin.debug: - msg: >- - WARN: Existing SAP instances detected in saphostctrl output for {{ sap_swpm_sid }}: - {{ __sap_swpm_fact_sap_instances_found | join(', ') }} - when: __sap_swpm_fact_sap_instances_found | length > 0 - + ignore_errors: true -## 2. Check contents of /usr/sap/sapservices -# This file can be managed by cluster software like Veritas, -# therefore not showing instance if cluster is stopped. -- name: Block for sapservices checks - when: __sap_swpm_detect_instance_numbers | d([]) | length > 0 - block: - - name: SAP SWPM Pre Install - Detect - Stat /usr/sap/sapservices file - ansible.builtin.stat: - path: /usr/sap/sapservices - register: __sap_swpm_register_sapservices_stat - - - name: SAP SWPM Pre Install - Detect - Read /usr/sap/sapservices file - ansible.builtin.slurp: - path: /usr/sap/sapservices - register: __sap_swpm_register_sapservices_slurp - when: __sap_swpm_register_sapservices_stat.stat.exists - - # Match pattern: SAP_ - - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in sapservices file - ansible.builtin.set_fact: - __sap_swpm_fact_sap_services_found: >- - {%- set content = (__sap_swpm_register_sapservices_slurp.content | b64decode) - if __sap_swpm_register_sapservices_slurp.content is defined else '' -%} - {%- set found = [] -%} - {%- for line in content.splitlines() -%} - {%- for nr in __sap_swpm_detect_instance_numbers -%} - {%- if ('SAP' ~ sap_swpm_sid ~ '_' ~ nr) in line -%} - {%- set _ = found.append(nr) -%} - {%- endif -%} - {%- endfor -%} - {%- endfor -%} - {{ found | unique }} - - - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in sapservices + - name: SAP SWPM Pre Install - Detect - Show reason why sapcontrol check was skipped - unavailable ansible.builtin.debug: - msg: >- - WARN: Existing SAP instances detected in sapservices file for {{ sap_swpm_sid }}: - {{ __sap_swpm_fact_sap_services_found | join(', ') }} - when: __sap_swpm_fact_sap_services_found | length > 0 + msg: | + INFO: Detection of existing SAP instance installation with 'sapcontrol' command was skipped. + Command 'sapcontrol' is not available for user '{{ __sap_swpm_sidadm_user }}'. + when: __sap_swpm_register_which_sapcontrol.rc != 0 -## 3. Check contents of /usr/sap// directories -- name: Block for directories checks - when: __sap_swpm_detect_instance_names | d([]) | length > 0 - block: - - name: SAP SWPM Pre Install - Detect - Stat directory {{ '/usr/sap/' ~ sap_swpm_sid ~ '/' }} - ansible.builtin.stat: - path: "{{ '/usr/sap/' ~ sap_swpm_sid ~ '' }}" - register: __sap_swpm_register_sapmnt_stat - - - name: SAP SWPM Pre Install - Detect - Find instance directories in {{ '/usr/sap/' ~ sap_swpm_sid ~ '/' }} - ansible.builtin.find: - paths: "/usr/sap/{{ sap_swpm_sid }}/" - file_type: directory - recurse: false - register: __sap_swpm_register_instance_dirs_find - when: __sap_swpm_register_sapmnt_stat.stat.exists - - # This task can take longer on installed instances. - # It took around 1 minute to get 14000 files in S4HANA 2025 PAS instance. - - name: SAP SWPM Pre Install - Detect - Check for files in matching directories - ansible.builtin.find: - paths: "{{ dir_item.path }}" - file_type: file - recurse: true - loop: "{{ __sap_swpm_register_instance_dirs_find.files | d([]) }}" - loop_control: - label: "{{ dir_item.path }}" - loop_var: dir_item - register: __sap_swpm_register_instance_dirs_content - when: dir_item.path | basename in __sap_swpm_detect_instance_names - - - name: SAP SWPM Pre Install - Detect - Display status of matching directories - ansible.builtin.debug: - msg: >- - Directory {{ file_item.dir_item.path }} contains {{ file_item.matched }} files. - loop: "{{ __sap_swpm_register_instance_dirs_content.results }}" - loop_control: - label: "{{ file_item.dir_item.path }}" - loop_var: file_item - when: - - file_item.skipped is not defined - - file_item.matched is defined - - - name: SAP SWPM Pre Install - Detect - Set fact if matching directories with files were found - ansible.builtin.set_fact: - __sap_swpm_fact_sap_directories_found: true - when: - - __sap_swpm_register_instance_dirs_content.results - | selectattr('matched', 'defined') - | selectattr('matched', 'gt', 0) - | list | length > 0 - - - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in directories - ansible.builtin.debug: - msg: | - WARN: Existing SAP instances detected in directories for {{ sap_swpm_sid }}: - {% for item in __sap_swpm_register_instance_dirs_content.results if item.matched is defined and item.matched > 0 %} - - Directory '{{ item.dir_item.path }}' contains {{ item.matched }} files. - {% endfor %} - when: __sap_swpm_fact_sap_directories_found | d(false) +# Check 6: Check presence of files in directories. +- name: SAP SWPM Pre Install - Detect - Stat directory {{ '/usr/sap/' ~ __sap_swpm_sid ~ '/' }} + ansible.builtin.stat: + path: "{{ '/usr/sap/' ~ __sap_swpm_sid ~ '' }}" + register: __sap_swpm_register_sapmnt_stat +- name: SAP SWPM Pre Install - Detect - Find instance directories in {{ '/usr/sap/' ~ __sap_swpm_sid ~ '/' }} + ansible.builtin.find: + paths: "/usr/sap/{{ __sap_swpm_sid }}/" + file_type: directory + recurse: false + register: __sap_swpm_register_instance_dirs_find + when: __sap_swpm_register_sapmnt_stat.stat.exists -## 4. Check profile files in /sapmnt//profile/ -- name: Block for profile files checks - when: __sap_swpm_detect_instance_names | d([]) | length > 0 - block: - - name: SAP SWPM Pre Install - Detect - Stat directory {{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }} - ansible.builtin.stat: - path: "{{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }}" - register: __sap_swpm_register_profile_dir_stat - - - name: SAP SWPM Pre Install - Detect - Find profile files in {{ '/sapmnt/' ~ sap_swpm_sid ~ '/profile/' }} - ansible.builtin.find: - paths: "/sapmnt/{{ sap_swpm_sid }}/profile/" - file_type: file - recurse: false - register: __sap_swpm_register_profiles_find - when: __sap_swpm_register_profile_dir_stat.stat.exists - - # Match pattern: __ (e.g., AE1_ASCS00_) - - name: SAP SWPM Pre Install - Detect - Set fact with matching SAP instances in profile files - ansible.builtin.set_fact: - __sap_swpm_fact_sap_profiles_found: >- - {%- set found = [] -%} - {%- for file in __sap_swpm_register_profiles_find.files | d([]) -%} - {%- for name in __sap_swpm_detect_instance_names -%} - {%- if (sap_swpm_sid ~ '_' ~ name ~ '_') in file.path | basename -%} - {%- set _ = found.append(file.path | basename) -%} - {%- endif -%} - {%- endfor -%} - {%- endfor -%} - {{ found | unique }} - - - name: SAP SWPM Pre Install - Detect - Inform if SAP instance is already present in profile files - ansible.builtin.debug: - msg: | - WARN: Existing SAP instances detected in profile files for {{ sap_swpm_sid }}: - {% for profile in __sap_swpm_fact_sap_profiles_found %} - - {{ profile }} - {% endfor %} - when: __sap_swpm_fact_sap_profiles_found | length > 0 +# Check 7: Check contents of /usr/sap/sapservices +# This file can be managed by cluster software like Veritas, +# therefore instances may not appear if cluster is stopped. +- name: SAP SWPM Pre Install - Detect - Stat /usr/sap/sapservices file + ansible.builtin.stat: + path: /usr/sap/sapservices + register: __sap_swpm_register_sapservices_stat + +- name: SAP SWPM Pre Install - Detect - Read /usr/sap/sapservices file + ansible.builtin.slurp: + path: /usr/sap/sapservices + register: __sap_swpm_register_sapservices_slurp + when: __sap_swpm_register_sapservices_stat.stat.exists + + +# Check 8: Check profile files in /sapmnt//profile/ +- name: SAP SWPM Pre Install - Detect - Stat directory {{ '/sapmnt/' ~ __sap_swpm_sid ~ '/profile/' }} + ansible.builtin.stat: + path: "{{ '/sapmnt/' ~ __sap_swpm_sid ~ '/profile/' }}" + register: __sap_swpm_register_profile_dir_stat + +- name: SAP SWPM Pre Install - Detect - Find profile files in {{ '/sapmnt/' ~ __sap_swpm_sid ~ '/profile/' }} + ansible.builtin.find: + paths: "/sapmnt/{{ __sap_swpm_sid }}/profile/" + file_type: file + recurse: false + register: __sap_swpm_register_profiles_find + when: __sap_swpm_register_profile_dir_stat.stat.exists + + +# Include task file for each instance. +- name: SAP SWPM Pre Install - Detect - Include tasks for each instance + ansible.builtin.include_tasks: + file: detect_sap_instance.yml + loop: "{{ __sap_swpm_detect_instances }}" + loop_control: + loop_var: instance_item + label: "{{ instance_item.number }}" + when: __sap_swpm_detect_instances is defined + + +### Decision section based on collected facts. +- name: SAP SWPM Pre Install - Detect - Show detection results in verbose mode + ansible.builtin.debug: + var: __sap_swpm_instance_results + when: ansible_verbosity >= 2 -- name: SAP SWPM Pre Install - Detect - Set fact if clues to existing SAP instance were found +# Set boolean facts for easier decision making in the next steps, based on collected results for all instances. +- name: SAP SWPM Pre Install - Detect - Set fact with results for all instances ansible.builtin.set_fact: - __sap_swpm_sap_detected: true + # SKIP should run only if all expected instances are detected. + __sap_swpm_fact_detect_saphostctrl_all_found: >- + {{ __sap_swpm_instance_results | selectattr('saphostctrl', 'equalto', true) | list | length == __sap_swpm_detect_instances | length }} + __sap_swpm_fact_detect_sapcontrol_all_found: >- + {{ __sap_swpm_instance_results | selectattr('sapcontrol', 'equalto', true) | list | length == __sap_swpm_detect_instances | length }} + # FAIL should run if any of the indicators is found, but SKIP is not triggered. + __sap_swpm_fact_detect_files_any_found: >- + {{ __sap_swpm_instance_results | selectattr('files', 'equalto', true) | list | length > 0 }} + __sap_swpm_fact_detect_sapservices_any_found: >- + {{ __sap_swpm_instance_results | selectattr('sapservices', 'equalto', true) | list | length > 0 }} + __sap_swpm_fact_detect_profiles_any_found: >- + {{ __sap_swpm_instance_results | selectattr('profiles', 'equalto', true) | list | length > 0 }} + + +# 3. FAIL - 'root' saphostctrl check does not find the desired SID and instance number, and any other indicators are found +# 6. FAIL - 'sidadm' sapcontrol check does not find the desired SID and instance number, and any other indicators are found +- name: SAP SWPM Pre Install - Detect - Fail if potential installation was detected + ansible.builtin.fail: + msg: | + FAIL: Potential existing SAP System installation detected based on the presence of identifiable indicators. + {% if __sap_swpm_fact_detect_files_any_found | d(false) %} + - Existence of files in '/usr/sap/{{ __sap_swpm_sid }}/' directory. + {% endif %} + {% if __sap_swpm_fact_detect_sapservices_any_found | d(false) %} + - Existence of instance entries in /usr/sap/sapservices file. + {% endif %} + {% if __sap_swpm_fact_detect_profiles_any_found | d(false) %} + - Existence of profile files in '/sapmnt/{{ __sap_swpm_sid }}/profile/' directory. + {% endif %} + These checks trigger failure only when 'saphostctrl' and 'sapcontrol' fail to detect all instances. + + Please verify the details above and ensure that you are not trying to install over an existing installation. + If you want to proceed with the installation regardless, please set 'sap_swpm_force' variable to true. when: - not sap_swpm_force | d(false) - - __sap_swpm_fact_sap_instances_found | d([]) | length > 0 - or __sap_swpm_fact_sap_services_found | d([]) | length > 0 - or __sap_swpm_fact_sap_profiles_found | d([]) | length > 0 - or __sap_swpm_fact_sap_directories_found | d(false) + - not __sap_swpm_fact_detect_saphostctrl_all_found | d(false) + - not __sap_swpm_fact_detect_sapcontrol_all_found | d(false) + - __sap_swpm_fact_detect_files_any_found | d(false) + or __sap_swpm_fact_detect_sapservices_any_found | d(false) + or __sap_swpm_fact_detect_profiles_any_found | d(false) -# This is very dynamic message that covers all possible scenarios. -- name: SAP SWPM Pre Install - Detect - Inform about next steps if SAP instance was detected - ansible.builtin.pause: - seconds: "{{ 30 if not sap_swpm_force | d(false) else 5 }}" - prompt: | - WARN: Clues to existing SAP instance(s) were detected for {{ sap_swpm_sid }}. - Product ID: '{{ sap_swpm_product_catalog_id | d('') }}' - {% if sap_swpm_force | d(false) %} - SWPM installation steps will be executed regardless, - because 'sap_swpm_force' variable is set to 'true' (default is 'false'). - {% else %} - SWPM installation steps will be skipped to avoid potential issues with existing installation. - - If you intend to proceed with the installation despite this warning, - set 'sap_swpm_force' variable to 'true' and execute this Ansible Role again. - {% endif %} +# 1. SKIP - 'root' saphostctrl check finds the desired SID and instance number. +# 4. SKIP - 'sidadm' sapcontrol check finds the desired SID and instance number. +- name: Block for SKIP + when: + # If saphostctrl is available, all instances must be detected from it, + # otherwise sapcontrol must have detected all instances. + - __sap_swpm_fact_detect_saphostctrl_all_found + or __sap_swpm_fact_detect_sapcontrol_all_found + block: + - name: SAP SWPM Pre Install - Detect - Set fact if installation was detected + ansible.builtin.set_fact: + __sap_swpm_sap_detected: true - SAP Instances were detected based on the following checks: - {% if __sap_swpm_fact_sap_instances_found | d([]) | length > 0 %} - - Output of '/usr/sap/hostctrl/exe/hostctrl -function ListInstances'. - {% endif %} - {% if __sap_swpm_fact_sap_services_found | d([]) | length > 0 %} - - Contents of '/usr/sap/services' file. - {% endif %} - {% if __sap_swpm_fact_sap_profiles_found | d([]) | length > 0 %} - - Existence of profile files in '/sapmnt/{{ sap_swpm_sid }}/profile/' directory. - {% endif %} - {% if __sap_swpm_fact_sap_directories_found | d(false) %} - - Existence of files in '/usr/sap/{{ sap_swpm_sid }}/' directories. - {% endif %} - when: __sap_swpm_sap_detected | d(false) + - name: SAP SWPM Pre Install - Detect - Inform that installation was detected + ansible.builtin.debug: + msg: | + {% if not sap_swpm_force %} + WARN: Existing SAP System installation detected using 'saphostctrl' or 'sapcontrol' command checks. + Installation steps will be skipped! + + If you intend to proceed with the installation despite this warning, + set 'sap_swpm_force' variable to 'true' and execute this Ansible Role again. + {% else %} + INFO: Existing SAP System installation detected using 'saphostctrl' or 'sapcontrol' command checks. + Installation steps will be executed regardless, + because 'sap_swpm_force' variable is set to 'true' (default is 'false'). + {% endif %} + + +# 2. INSTALL - 'root' saphostctrl check does not find the desired SID and instance number, and no other indicators are found. +# 5. INSTALL - 'sidadm' sapcontrol check does not find the desired SID and instance number, and no other indicators are found. +- name: SAP SWPM Pre Install - Detect - Inform that no existing installation was detected + ansible.builtin.debug: + msg: | + INFO: No existing SAP System installation detected for SID {{ __sap_swpm_sid }}. + Installation will proceed. + + Detection methods used: + - saphostctrl command (ListInstances) + - sapcontrol command (GetInstanceProperties) + - Instance directories (/usr/sap/{{ __sap_swpm_sid }}/) + - SAP services file (/usr/sap/sapservices) + - Profile files (/sapmnt/{{ __sap_swpm_sid }}/profile/) + when: + - not __sap_swpm_fact_detect_saphostctrl_all_found | d(false) + - not __sap_swpm_fact_detect_sapcontrol_all_found | d(false) + - not __sap_swpm_fact_detect_files_any_found | d(false) + - not __sap_swpm_fact_detect_sapservices_any_found | d(false) + - not __sap_swpm_fact_detect_profiles_any_found | d(false) # We cannot reliably detect some products, diff --git a/roles/sap_swpm/tasks/pre_install/detect_sap_instance.yml b/roles/sap_swpm/tasks/pre_install/detect_sap_instance.yml new file mode 100644 index 00000000..6430c33c --- /dev/null +++ b/roles/sap_swpm/tasks/pre_install/detect_sap_instance.yml @@ -0,0 +1,232 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +### This is a dedicated file to perform per-instance detection checks. +- name: SAP SWPM Pre Install - Detect - Reset detection facts before checks + ansible.builtin.set_fact: + __sap_swpm_fact_detect_saphostctrl_instance_found: false + __sap_swpm_fact_detect_sapcontrol_instance_found: false + __sap_swpm_fact_detect_instance_files: false + __sap_swpm_fact_detect_instance_sapservices_found: false + __sap_swpm_fact_detect_instance_profiles_found: false + + +# Check 4: Detect 'saphostctrl' and search for SID and instance number in 'ListInstances' output. +# saphostctrl can return non-zero exit code when sapstartsrv process is not running. +- name: Block if saphostctrl ListInstances command executed successfully + when: + - __sap_swpm_register_saphostctrl_list_instances is defined + - __sap_swpm_register_saphostctrl_list_instances.rc is defined + - __sap_swpm_register_saphostctrl_list_instances.rc == 0 + block: + + # Match pattern: " - ". + # Example: Inst Info : AE1 - 10 - ae1ers-ha - 793, patch 51, commit 375ae433dffb023985041c1d7b4687b1bde8d211 + - name: SAP SWPM Pre Install - Detect - Set fact if instance is detected in saphostctrl output - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_saphostctrl_instance_found: >- + {{ __sap_swpm_register_saphostctrl_list_instances.stdout_lines | d([]) + | select('search', ' ' ~ __sap_swpm_sid ~ ' - ' ~ instance_item.number ~ ' ') + | list | length > 0 }} + + # Check for conflict: instance number exists but with a different SID. + - name: SAP SWPM Pre Install - Detect - Fail if instance number conflict was found (Same NR, Different SID) - {{ instance_item.number | d('') }} + ansible.builtin.fail: + msg: | + FAIL: Conflict detected for instance number {{ instance_item.number }}! + This instance number is already used by a different SID. + Expected SID {{ __sap_swpm_sid }}, but found following lines: + {% for line in __conflict_lines %} + - {{ line }} + {% endfor %} + when: __conflict_lines | length > 0 + vars: + __conflict_lines: >- + {{ __sap_swpm_register_saphostctrl_list_instances.stdout_lines | d([]) + | select('search', ' - ' ~ instance_item.number ~ ' ') + | reject('search', ' ' ~ __sap_swpm_sid ~ ' - ') + | list }} + + +# Check 5: Use 'sapcontrol' and search for SID and instance number in 'GetInstanceProperties' output. +- name: Block if sapcontrol is available + when: + - __sap_swpm_register_which_sapcontrol is defined + - __sap_swpm_register_which_sapcontrol.rc is defined + - __sap_swpm_register_which_sapcontrol.rc == 0 + block: + + # Reason for noqa: Switch to user requires shell module to load environment variables. + - name: SAP SWPM Pre Install - Detect - Execute sapcontrol - {{ instance_item.number | d('') }} # noqa: command-instead-of-shell + ansible.builtin.shell: + cmd: "bash -lc 'sapcontrol -nr {{ instance_item.number }} -function GetInstanceProperties'" + become: true + become_user: "{{ __sap_swpm_sidadm_user }}" + register: __sap_swpm_register_sapcontrol_instance_properties + changed_when: false + # Hides failed output, but 'rc' can be still used for verification. + failed_when: false + + - name: SAP SWPM Pre Install - Detect - Show reason why sapcontrol check was skipped - command failed - {{ instance_item.number | d('') }} + ansible.builtin.debug: + msg: | + INFO: Detection of existing SAP instance installation with 'sapcontrol' command was skipped. + Return code: {{ __sap_swpm_register_sapcontrol_instance_properties.rc | d('1') }} + Output: {{ __sap_swpm_register_sapcontrol_instance_properties.stdout | d('') }} + Error: {{ __sap_swpm_register_sapcontrol_instance_properties.stderr | d('') }} + when: + - __sap_swpm_register_sapcontrol_instance_properties.rc is defined + - __sap_swpm_register_sapcontrol_instance_properties.rc != 0 + + # Check if output contains correct SAPSYSTEMNAME. + - name: SAP SWPM Pre Install - Detect - Set fact if matching SID is found in sapcontrol output - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_sapcontrol_sid_match: >- + {{ __sap_swpm_register_sapcontrol_instance_properties.stdout_lines | d([]) + | select('search', 'SAPSYSTEMNAME.*,.*' ~ __sap_swpm_sid) + | list | length > 0 }} + + # Extract INSTANCE_NAME value from output. + - name: SAP SWPM Pre Install - Detect - Set fact with discovered instance name from sapcontrol output - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_sapcontrol_instance_name: "{{ __parsed_instance_name }}" + when: __sap_swpm_fact_detect_sapcontrol_sid_match + vars: + # Extract the line containing INSTANCE_NAME. + __instance_name_line: >- + {{ __sap_swpm_register_sapcontrol_instance_properties.stdout_lines | d([]) + | select('search', 'INSTANCE_NAME') + | first | d('') }} + # Extract the value after the last comma, trimming whitespaces/quotes. + __parsed_instance_name: "{{ __instance_name_line.split(',')[-1] | trim }}" + + # Check if discovered name is in the list of expected names (ANY match = detected). + - name: SAP SWPM Pre Install - Detect - Set fact instance is detected in sapcontrol output - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_sapcontrol_instance_found: >- + {{ __sap_swpm_fact_detect_sapcontrol_sid_match + and __sap_swpm_fact_detect_sapcontrol_instance_name | d('') in instance_item.names }} + + - name: SAP SWPM Pre Install - Detect - Show sapcontrol command output in verbose mode + ansible.builtin.debug: + var: __sap_swpm_register_sapcontrol_instance_properties + when: ansible_verbosity >= 2 + + +# Check 6: Check presence of files in directories. +# Filter directories that match this instance's names. +- name: SAP SWPM Pre Install - Detect - Set fact with filtered directories matching instance {{ instance_item.number }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_instance_dirs_matching: >- + {{ __sap_swpm_register_instance_dirs_find.files | d([]) + | selectattr('path', 'defined') + | list + | selectattr('path', 'match', '^.*/(' ~ instance_item.names | join('|') ~ ')$') + | list }} + +# Check for files in matching directories (can take ~1 minute for installed instances). +# It took around 1 minute to get 14000 files in S4HANA 2025 PAS instance. +- name: SAP SWPM Pre Install - Detect - Find files in directories - {{ instance_item.number }} + ansible.builtin.find: + paths: "{{ dir_item.path }}" + file_type: file + recurse: true + loop: "{{ __sap_swpm_fact_detect_instance_dirs_matching }}" + loop_control: + label: "{{ dir_item.path }}" + loop_var: dir_item + register: __sap_swpm_register_detect_instance_files_found + when: __sap_swpm_fact_detect_instance_dirs_matching | length > 0 + +- name: SAP SWPM Pre Install - Detect - Set fact if instance directories contain existing files - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_instance_files: >- + {{ __sap_swpm_register_detect_instance_files_found.results | d([]) + | selectattr('matched', 'defined') + | selectattr('matched', 'gt', 0) + | list | length > 0 }} + when: __sap_swpm_register_detect_instance_files_found is defined + + +# Check 7: Check contents of /usr/sap/sapservices +- name: Block for sapservices checks + when: + - __sap_swpm_register_sapservices_slurp is defined + - __sap_swpm_register_sapservices_slurp.content is defined + block: + + # Match pattern: SAP_ for this instance + - name: SAP SWPM Pre Install - Detect - Set fact if instance is detected in sapservices - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_instance_sapservices_found: >- + {{ (__sap_swpm_register_sapservices_slurp.content | b64decode) + | regex_search('SAP' ~ __sap_swpm_sid ~ '_' ~ instance_item.number) is not none }} + + +# Check 8: Check profile files in /sapmnt//profile/ +- name: Block for profile files checks + when: + - __sap_swpm_register_profiles_find is defined + - __sap_swpm_register_profiles_find.files is defined + block: + + # Match pattern: __ (e.g., AE1_ASCS00_) + # Check if any profile file matches any of this instance's expected names. + - name: SAP SWPM Pre Install - Detect - Set fact if instance is detected in profile files - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_fact_detect_instance_profiles_found: >- + {{ __sap_swpm_register_profiles_find.files | d([]) + | map(attribute='path') + | map('basename') + | select('match', '^' ~ __sap_swpm_sid ~ '_(' ~ instance_item.names | join('|') ~ ')_') + | list | length > 0 }} + + +# Display detection results for this instance +- name: SAP SWPM Pre Install - Detect - Show detection results - {{ instance_item.number | d('') }} + ansible.builtin.debug: + msg: | + INFO: Detection results for instance {{ instance_item.number }} ({{ instance_item.names | join(', ') }}): + {% if __sap_swpm_fact_detect_saphostctrl_instance_found | d(false) %} + - Found in saphostctrl output (ListInstances). + {% endif %} + {% if __sap_swpm_fact_detect_sapcontrol_instance_found | d(false) %} + - Found in sapcontrol output (GetInstanceProperties). + {% endif %} + {% if __sap_swpm_fact_detect_instance_files | d(false) %} + - Found files in instance directories (/usr/sap/{{ __sap_swpm_sid }}/): + {% for dir in __sap_swpm_register_detect_instance_files_found.results if dir.matched is defined and dir.matched > 0 %} + '{{ dir.dir_item.path }}': {{ dir.matched }} files. + {% endfor %} + {% endif %} + {% if __sap_swpm_fact_detect_instance_sapservices_found | d(false) %} + - Found in SAP services file (/usr/sap/sapservices). + {% endif %} + {% if __sap_swpm_fact_detect_instance_profiles_found | d(false) %} + - Found in profile files (/sapmnt/{{ __sap_swpm_sid }}/profile/): + {% endif %} + {% if not __any_checks_true %} + - No existing installation detected for this instance + {% endif %} + vars: + __any_checks_true: >- + {{ __sap_swpm_fact_detect_saphostctrl_instance_found | d(false) + or __sap_swpm_fact_detect_sapcontrol_instance_found | d(false) + or __sap_swpm_fact_detect_instance_files | d(false) + or __sap_swpm_fact_detect_instance_sapservices_found | d(false) + or __sap_swpm_fact_detect_instance_profiles_found | d(false) }} + when: __any_checks_true + + +- name: SAP SWPM Pre Install - Detect - Set fact with detection results - {{ instance_item.number | d('') }} + ansible.builtin.set_fact: + __sap_swpm_instance_results: "{{ __sap_swpm_instance_results | d([]) + [__current_result] }}" + vars: + __current_result: + number: "{{ instance_item.number }}" + names: "{{ instance_item.names }}" + saphostctrl: "{{ __sap_swpm_fact_detect_saphostctrl_instance_found | d(false) }}" + sapcontrol: "{{ __sap_swpm_fact_detect_sapcontrol_instance_found | d(false) }}" + files: "{{ __sap_swpm_fact_detect_instance_files | d(false) }}" + sapservices: "{{ __sap_swpm_fact_detect_instance_sapservices_found | d(false) }}" + profiles: "{{ __sap_swpm_fact_detect_instance_profiles_found | d(false) }}" diff --git a/roles/sap_swpm/tasks/pre_install/detect_sap_prepare.yml b/roles/sap_swpm/tasks/pre_install/detect_sap_prepare.yml new file mode 100644 index 00000000..685ed9e9 --- /dev/null +++ b/roles/sap_swpm/tasks/pre_install/detect_sap_prepare.yml @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: Apache-2.0 +--- + +# Prepare lists of instance names and numbers for detection based on provided variables and product catalog ID. + +# Extract prefix from Product ID, or inform that prefix is not recognized. +- name: SAP SWPM Pre Install - Set fact with SAP Product ID prefix + ansible.builtin.set_fact: + __sap_swpm_fact_product_prefix: >- + {{ sap_swpm_product_catalog_id.split(':') | first + if sap_swpm_product_catalog_id | d('') | trim | length > 0 and ':' in sap_swpm_product_catalog_id + else '' }} + +- name: SAP SWPM Pre Install - Detect - Inform if SAP Product ID prefix is unavailable + ansible.builtin.debug: + msg: | + WARN: Unable to extract SAP Product ID prefix for product catalog ID '{{ sap_swpm_product_catalog_id }}'. + Expected format of product catalog ID is 'PREFIX:REST_OF_ID' (e.g., 'NW_ABAP_ASCS:S4HANA2025.CORE.HDB.ABAP'). + Detection of existing SAP instances will proceed based on contents of 'sap_swpm_inifile_sections_list' variable. + + NOTE: Providing invalid SAP Product ID will most likely lead into failure during SWPM execution. + when: __sap_swpm_fact_product_prefix | length == 0 + + +# Prepare lists of instance names and numbers for detection. +# Lists based on Product ID. +# Regex is used to remove items with empty values. +- name: SAP SWPM Pre Install - Detect - Set fact with lists for detection using product ID + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instances'] }}" + when: + - __sap_swpm_fact_product_prefix | trim | length > 0 + - __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix] is defined + - __sap_swpm_product_sections_dict[__sap_swpm_fact_product_prefix]['instances'] is defined + + +# Lists based on sections list if Product ID cannot be used. +# NOTE: Defaults before 'unique' do not serve any purpose, because when already validates it, +# but it is required for linting to pass: +# Falling back to Ansible unique filter as Jinja2 one failed: 'sap_swpm_ascs_instance_nr' is undefined +- name: Block to fall back on sections if product ID is not in dictionary + when: + - __sap_swpm_fact_product_prefix | trim | length == 0 + or __sap_swpm_fact_product_prefix not in ( + __sap_swpm_product_sections_dict.keys() | list + + __sap_swpm_product_detection_skipped | dict2items | map(attribute='value') | flatten) + block: + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for ASCS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_detect_instances | d([]) + __ascs_instances }}" + vars: + __ascs_instances: + - number: "{{ sap_swpm_ascs_instance_nr }}" + names: + - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr }}" + when: + - sap_swpm_ascs_instance_nr | d('') | length > 0 + - "'nw_config_central_services_abap' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for SCS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_detect_instances | d([]) + __scs_instances }}" + vars: + __scs_instances: + - number: "{{ sap_swpm_java_scs_instance_nr }}" + names: + - "{{ 'SCS' ~ sap_swpm_java_scs_instance_nr }}" + when: + - sap_swpm_java_scs_instance_nr | d('') | length > 0 + - "'nw_config_central_services_java' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for ERS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_detect_instances | d([]) + __ers_instances }}" + vars: + __ers_instances: + - number: "{{ sap_swpm_ers_instance_nr }}" + names: + - "{{ 'ERS' ~ sap_swpm_ers_instance_nr }}" + when: + - sap_swpm_ers_instance_nr | d('') | length > 0 + - "'nw_config_ers' in sap_swpm_inifile_sections_list" + + # Since 'nw_config_primary_application_server_instance' is not ABAP specific, + # we need to check for both ABAP and JAVA. + - name: SAP SWPM Pre Install - Detect - Set fact with lists for PAS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_detect_instances | d([]) + __pas_instances }}" + vars: + __pas_instances: + - number: "{{ sap_swpm_pas_instance_nr }}" + names: + - "{{ 'D' ~ sap_swpm_pas_instance_nr }}" + # DVEBMGS is used as fallback to legacy instance name. + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr }}" + - "{{ 'J' ~ sap_swpm_pas_instance_nr }}" + when: + - sap_swpm_pas_instance_nr | d('') | length > 0 + - "'nw_config_primary_application_server_instance' in sap_swpm_inifile_sections_list" + + - name: SAP SWPM Pre Install - Detect - Set fact with lists for AAS detection + ansible.builtin.set_fact: + __sap_swpm_detect_instances: + "{{ __sap_swpm_detect_instances | d([]) + __aas_instances }}" + vars: + __aas_instances: + - number: "{{ sap_swpm_aas_instance_nr }}" + names: + - "{{ 'D' ~ sap_swpm_aas_instance_nr }}" + - "{{ 'J' ~ sap_swpm_aas_instance_nr }}" + when: + - sap_swpm_aas_instance_nr | d('') | length > 0 + - "'nw_config_additional_application_server_instance' in sap_swpm_inifile_sections_list" diff --git a/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml b/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml index 15857ece..ee627b8d 100644 --- a/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml +++ b/roles/sap_swpm/tasks/pre_install/swpm_prepare.yml @@ -27,14 +27,35 @@ # Detect running SAP instances using variables validated in tasks above. # NOTE: This will result in skipping of remaining tasks, unless force is used. -- name: SAP SWPM Pre Install - Detect running SAP instances - ansible.builtin.include_tasks: - file: pre_install/detect_sap.yml +- name: Block for SAP Detection # Inherited tags are explicitly declared here to ensure this inclusion # is skipped (and logs are hidden) when only sub-tags like 'sap_swpm_generate_inifile' are requested. tags: - sap_swpm_pre_install - sap_swpm_install + block: + - name: SAP SWPM Pre Install - Skip all detection checks + ansible.builtin.debug: + msg: | + WARN: Detection checks are SKIPPED because 'sap_swpm_skip_detection' is set to 'true'. + + This bypasses all safety mechanisms that prevent installation over existing SAP systems. + Use this ONLY for testing/development on clean systems. + + Proceeding with installation without any detection checks. + when: sap_swpm_skip_detection | d(false) + + - name: SAP SWPM Pre Install - Prepare variables for SAP detection + ansible.builtin.include_tasks: + file: detect_sap_prepare.yml + when: not sap_swpm_skip_detection | d(false) + + - name: SAP SWPM Pre Install - Detect running SAP instances + ansible.builtin.include_tasks: + file: pre_install/detect_sap.yml + when: + - not sap_swpm_skip_detection | d(false) + - __sap_swpm_detect_instances | d([]) | length > 0 - name: Block to execute only if SAP instance is not detected or in force mode. diff --git a/roles/sap_swpm/vars/main.yml b/roles/sap_swpm/vars/main.yml index 4c9ade65..5385c8ea 100644 --- a/roles/sap_swpm/vars/main.yml +++ b/roles/sap_swpm/vars/main.yml @@ -48,16 +48,17 @@ __sap_swpm_firewall_service_name_app: "sap-nw-app" # Boolean state of detection of SAP Instances. __sap_swpm_sap_detected: false -# List of SAP instance numbers generated in pre_install/detect_sap.yml. -__sap_swpm_detect_instance_numbers: [] - -# List of expected SAP instance names generated in pre_install/detect_sap.yml. -__sap_swpm_detect_instance_names: [] +# List of combinations of SAP instance numbers and names generated in pre_install/detect_sap_prepare.yml. +__sap_swpm_detect_instances: [] # Dictionary Matrix of product prefixes and sections combinations that must be enforced. # This will ensure that required sections are present for specific products, # while allowing other products to pass through and potentially fail during SWPM. # NOTE: Dictionary contains variables that will be expanded only when accessed, which is after validation of inifile! +# - sections - list of sections that must be present in inifile for this product. +# - instances - list of dictionaries with 'number' and 'names' keys to define expected instance number and name format for the instance. +# - number - unique instance number for the instance. +# - names - list of possible instance names for the instance. Any of them means that the instance is detected. __sap_swpm_product_sections_dict: # Product Catalog ID prefix. # Example: 'NW_ABAP_ASCS' for product catalog ID 'NW_ABAP_ASCS:S4HANA2025.CORE.HDB.ABAP'. @@ -66,70 +67,72 @@ __sap_swpm_product_sections_dict: # Name of section in 'sap_swpm_inifile_sections_list' that must be present for this product. sections: - nw_config_central_services_abap - # Instance name prefix used for detection of SAP instances. - instance_names: - - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr if sap_swpm_ascs_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_ascs_instance_nr | d('') }}" + instances: + - number: "{{ sap_swpm_ascs_instance_nr }}" + names: + - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr }}" - # Contains: PAS - NW_ABAP_CI: + # Contains: SCS + NW_Java_SCS: sections: - - nw_config_primary_application_server_instance - instance_names: - - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" - - "{{ 'D' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_pas_instance_nr | d('') }}" + - nw_config_central_services_java + instances: + - number: "{{ sap_swpm_java_scs_instance_nr }}" + names: + - "{{ 'SCS' ~ sap_swpm_java_scs_instance_nr }}" + + # Contains: ERS + NW_ERS: + sections: + - nw_config_ers + instances: + - number: "{{ sap_swpm_ers_instance_nr }}" + names: + - "{{ 'ERS' ~ sap_swpm_ers_instance_nr }}" # Contains: ASCS, PAS (ABAP) + # DVEBMGS is used as fallback to legacy instance name. NW_ABAP_OneHost: sections: - nw_config_central_services_abap - nw_config_primary_application_server_instance - instance_names: - - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr if sap_swpm_ascs_instance_nr | d('') | length > 0 else '' }}" - - "{{ 'D' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" - - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_ascs_instance_nr | d('') }}" - - "{{ sap_swpm_pas_instance_nr | d('') }}" + instances: + - number: "{{ sap_swpm_ascs_instance_nr }}" + names: + - "{{ 'ASCS' ~ sap_swpm_ascs_instance_nr }}" + - number: "{{ sap_swpm_pas_instance_nr }}" + names: + - "{{ 'D' ~ sap_swpm_pas_instance_nr }}" + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr }}" - # Contains: AAS (ABAP or JAVA) - NW_DI: - sections: - - nw_config_additional_application_server_instance - instance_names: - - "{{ 'D' ~ sap_swpm_aas_instance_nr if sap_swpm_aas_instance_nr | d('') | length > 0 else '' }}" - - "{{ 'J' ~ sap_swpm_aas_instance_nr if sap_swpm_aas_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_aas_instance_nr | d('') }}" - - # Contains: ERS - NW_ERS: + # Contains: PAS + NW_ABAP_CI: sections: - - nw_config_ers - instance_names: - - "{{ 'ERS' ~ sap_swpm_ers_instance_nr if sap_swpm_ers_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_ers_instance_nr | d('') }}" + - nw_config_primary_application_server_instance + instances: + - number: "{{ sap_swpm_pas_instance_nr }}" + names: + - "{{ 'DVEBMGS' ~ sap_swpm_pas_instance_nr }}" + - "{{ 'D' ~ sap_swpm_pas_instance_nr }}" # Contains: ASCS, PAS (JAVA) NW_Java_CI: sections: - nw_config_primary_application_server_instance - instance_names: - - "{{ 'J' ~ sap_swpm_pas_instance_nr if sap_swpm_pas_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_pas_instance_nr | d('') }}" + instances: + - number: "{{ sap_swpm_pas_instance_nr }}" + names: + - "{{ 'J' ~ sap_swpm_pas_instance_nr }}" - NW_Java_SCS: + # Contains: AAS (ABAP or JAVA) + NW_DI: sections: - - nw_config_central_services_java - instance_names: - - "{{ 'SCS' ~ sap_swpm_java_scs_instance_nr if sap_swpm_java_scs_instance_nr | d('') | length > 0 else '' }}" - instance_numbers: - - "{{ sap_swpm_java_scs_instance_nr | d('') }}" + - nw_config_additional_application_server_instance + instances: + - number: "{{ sap_swpm_aas_instance_nr }}" + names: + - "{{ 'D' ~ sap_swpm_aas_instance_nr }}" + - "{{ 'J' ~ sap_swpm_aas_instance_nr }}" # This is special list of product prefixes, where we cannot detect SAP, @@ -144,3 +147,9 @@ __sap_swpm_product_detection_skipped: - NW_Java_DB # Space for more categories, if more products are found. Candidates: # NW_ABAP_DB_DBRefresh, NW_Java_DB_DBRefresh, NW_Webdispatcher. + +# System ID (SID) in upper case. +__sap_swpm_sid: "{{ sap_swpm_sid | upper }}" + +# Name of 'adm' user. +__sap_swpm_sidadm_user: "{{ sap_swpm_sid | lower }}adm"