From c53e59f08d264cca6ab2b75c1e1b8dadc57c49c5 Mon Sep 17 00:00:00 2001 From: Nicolas Bettembourg Date: Fri, 18 Jul 2025 17:08:22 +0200 Subject: [PATCH 1/6] Add SAP System-level Function Support with Asynchronous Status Tracking (#1) * Adding support for system wide functions * Nested Jinja makes the use of dynamic variables inside sapcontrol functions complicated. Falling back to declarative commands. * Adding steps to make sure that the system is started before attempting RKS * Enhance debug output for SAP Control parameters --------- Signed-off-by: Nicolas Bettembourg Co-authored-by: Rainer Leber <39616583+rainerleber@users.noreply.github.com> --- roles/sap_control/README.md | 3 +- roles/sap_control/defaults/main.yml | 53 ++++++++++++++++++++ roles/sap_control/tasks/main.yml | 16 ++++-- roles/sap_control/tasks/prepare.yml | 15 ++++++ roles/sap_control/tasks/sapcontrol.yml | 8 +++ roles/sap_control/tasks/sapcontrol_async.yml | 29 +++++++++++ 6 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 roles/sap_control/tasks/sapcontrol_async.yml diff --git a/roles/sap_control/README.md b/roles/sap_control/README.md index 0409cef..8e513fb 100644 --- a/roles/sap_control/README.md +++ b/roles/sap_control/README.md @@ -7,6 +7,7 @@ This Ansible Role executes basic SAP administration tasks on Linux operating sys This Ansible Role executes basic SAP administration tasks on Linux operating systems, including: - Start/Stop/Restart of SAP HANA Database Server - Start/Stop/Restart of SAP NetWeaver Application Server +- Start/Stop/Restart/Update of SAP Netweaver System - Multiple Automatic discovery and Start/Stop/Restart of SAP HANA Database Server or SAP NetWeaver Application Server ## Example execution @@ -62,7 +63,7 @@ Assumptions for executing this role include: | :--- |:--- | :--- | | `SID` | SAP system SID | no, only if you are targetting a single SAP system| | `nowait` | Default: `false` | no, use only when absolutely sure! This will bypass all waiting and ignore all necessary steps for a graceful stop / start| -| `sap_control_function` | Function to execute:
| yes, only this is required to detect the Instance Number which is used with SAP Host Agent `sapcontrol` CLI


_Note: Executions using `all` will automatically detect any System IDs and corresponding Instance Numbers_ | +| `sap_control_function` | Function to execute:
| yes, only this is required to detect the Instance Number which is used with SAP Host Agent `sapcontrol` CLI


_Note: Executions using `all` will automatically detect any System IDs and corresponding Instance Numbers_ | ## Ansible Role workflow and structure diff --git a/roles/sap_control/defaults/main.yml b/roles/sap_control/defaults/main.yml index 07065d5..7f4d49b 100644 --- a/roles/sap_control/defaults/main.yml +++ b/roles/sap_control/defaults/main.yml @@ -6,6 +6,41 @@ sap_control_name_header: "initial" nowait: false sap_control_start: "StartWait 180 2" sap_control_stop: "StopWait 180 2" +sap_control_startsystem: "StartSystem ALL 180" # function StartSystem waittimeout +sap_control_stopsystem: "StopSystem ALL 180 480" # function StopSystem waittimeout softtimeout +sap_control_restartsystem: "RestartSystem ALL 180 480" # function RestartSystem waittimeout softtimeout +sap_control_updatesystem: "UpdateSystem 180 480 0" # function UpdateSystem waittimeout softtimeout force +sap_control_waitforstopped: "WaitforStopped 180 2" # function WaitforStopped waittimeout delay +sap_control_waitforstarted: "WaitforStarted 180 2" # function WaitforStarted waittimeout delay + +# Parameters to handle async functions in sapcontrol_async.yml + +sap_control_startsystem_waitforasync: + test_function: "GetSystemInstanceList" + retries: 60 + delay: 10 + until_false: 'GRAY\s*$|RED\s*$|YELLOW\s*$' + until_true: 'GREEN\s*$' + +sap_control_restartsystem_waitforasync: + test_function: "GetSystemInstanceList" + retries: 60 + delay: 10 + until_false: 'GRAY\s*$|RED\s*$|YELLOW\s*$' + until_true: 'GREEN\s*$' + +sap_control_stopsystem_waitforasync: + test_function: "GetSystemInstanceList" + retries: 60 + delay: 10 + until_false: 'GREEN\s*$|RED\s*$|YELLOW\s*$' + until_true: 'GRAY\s*$' + +sap_control_updatesystem_waitforasync: + test_function: "GetSystemUpdateList" + retries: 60 + delay: 10 + until_false: 'GRAY\s*$|RED\s*$|YELLOW\s*$|GREEN\s*$' # get_all_sap_sid_dir_nw: "/sapmnt" # get_all_sap_sid_dir_hana: "/hana/shared" @@ -22,6 +57,10 @@ sap_control_instance_type_sortorder: # Functions sap_control_functions_list: + - restartsystem_all_nw + - updatesystem_all_nw + - startsystem_all_nw + - stopsystem_all_nw - restart_all_sap - stop_all_sap - start_all_sap @@ -38,7 +77,21 @@ sap_control_functions_list: - stop_sap_hana - start_sap_hana + # Functions flow +restartsystem_all_nw_list: + - sap_control_function_current: "nw_restartsystem" + +startsystem_all_nw_list: + - sap_control_function_current: "nw_startsystem" + +stopsystem_all_nw_list: + - sap_control_function_current: "nw_stopsystem" + +updatesystem_all_nw_list: + - sap_control_function_current: "nw_startsystem" + - sap_control_function_current: "nw_updatesystem" + restart_all_sap_list: - sap_control_function_current: "nw_stop" - sap_control_function_current: "hana_stop" diff --git a/roles/sap_control/tasks/main.yml b/roles/sap_control/tasks/main.yml index 0204f63..ffde9fe 100644 --- a/roles/sap_control/tasks/main.yml +++ b/roles/sap_control/tasks/main.yml @@ -112,10 +112,18 @@ ansible.builtin.debug: msg: - "Starting sap_control with the following parameters: " - - "{{ sap_control_function }}" - - "{{ sap_control_start }}" - - "{{ sap_control_stop }}" - - "{{ nowait }}" + - "Function: {{ sap_control_function }}" + - "Standard commands:" + - " Start: {{ sap_control_start }}" + - " Stop: {{ sap_control_stop }}" + - "System commands (if applicable):" + - " StartSystem: {{ sap_control_startsystem }}" + - " StopSystem: {{ sap_control_stopsystem }}" + - " RestartSystem: {{ sap_control_restartsystem }}" + - " UpdateSystem: {{ sap_control_updatesystem }}" + - " WaitforStopped: {{ sap_control_waitforstopped }}" + - " WaitforStarted: {{ sap_control_waitforstarted }}" + - "NoWait: {{ nowait }}" # Start SAP Control - name: SAP Control diff --git a/roles/sap_control/tasks/prepare.yml b/roles/sap_control/tasks/prepare.yml index 91d718a..ff8dfef 100644 --- a/roles/sap_control/tasks/prepare.yml +++ b/roles/sap_control/tasks/prepare.yml @@ -36,3 +36,18 @@ loop: "{{ sorted_sap_facts }}" when: - item.InstanceType | lower == sap_type | lower + - not funct_type is match('.*system') + +- name: Prepare - SAP Control for system functions + vars: + sap_control_execute_sid: "{{ item.SID }}" + sap_control_execute_type: "{{ item.Type }}" + sap_control_execute_instance_nr: "{{ item.NR }}" + sap_control_execute_instance_type: "{{ item.InstanceType }}" + ansible.builtin.include_tasks: "sapcontrol.yml" + loop: "{{ sorted_sap_facts }}" + when: + - item.InstanceType | lower == sap_type | lower + - funct_type is match('.*system') + - item.TYPE | lower == 'ascs' + or item.TYPE | lower == 'scs' diff --git a/roles/sap_control/tasks/sapcontrol.yml b/roles/sap_control/tasks/sapcontrol.yml index af3cda8..aa181a2 100644 --- a/roles/sap_control/tasks/sapcontrol.yml +++ b/roles/sap_control/tasks/sapcontrol.yml @@ -22,6 +22,14 @@ register: sapcontrol_status failed_when: "'FAIL' in sapcontrol_status.stdout" +# Include sapcontrol async tasks +- name: SAP {{ sap_control_name_header }} - Include async tasks + vars: + async_function_dict: "{{ vars['sap_control_' + funct_type + '_waitforasync'] }}" + ansible.builtin.include_tasks: sapcontrol_async.yml + when: + - funct_type is match('.*system') + # Cleanipc - name: SAP {{ sap_control_name_header }} - Cleanipc ansible.builtin.include_tasks: functions/cleanipc.yml diff --git a/roles/sap_control/tasks/sapcontrol_async.yml b/roles/sap_control/tasks/sapcontrol_async.yml new file mode 100644 index 0000000..ebe65bc --- /dev/null +++ b/roles/sap_control/tasks/sapcontrol_async.yml @@ -0,0 +1,29 @@ +--- +- name: Pause for 5 Seconds + ansible.builtin.wait_for: + timeout: 5 + +- name: SAP {{ sap_control_name_header }} - Checking if Async action is over by executing sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} + ansible.builtin.shell: | + source ~/.profile && sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} + args: + executable: /bin/bash + become: true + become_user: "{{ passed_sap_sid | lower }}adm" + register: test_function_result +# failed_when: "'FAIL' in test_function_result.stdout" + retries: "{{ async_function_dict.retries | default(0) | int }}" + delay: "{{ async_function_dict.delay | default(0) | int }}" + until: > + (async_function_dict.until_false is not defined + or async_function_dict.until_false is defined and not test_function_result.stdout | regex_search(async_function_dict.until_false, multiline=True)) and + (async_function_dict.until_true is not defined or + async_function_dict.until_true is defined and test_function_result.stdout | regex_search(async_function_dict.until_true, multiline=True)) + failed_when: false + +- name: Debug stdout + ansible.builtin.debug: + msg: | + Async function {{ async_function_dict.test_function }} for SAP SID {{ passed_sap_sid }} + is done with result: + {{ test_function_result.stdout }} From 1cecfd6695b1b3e28a2321864d6bde63583cb074 Mon Sep 17 00:00:00 2001 From: Nicolas Bettembourg Date: Mon, 21 Jul 2025 13:04:05 +0200 Subject: [PATCH 2/6] Changelog fragment Signed-off-by: Nicolas Bettembourg --- changelogs/fragments/37_sap_control_add_system_functions | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/37_sap_control_add_system_functions diff --git a/changelogs/fragments/37_sap_control_add_system_functions b/changelogs/fragments/37_sap_control_add_system_functions new file mode 100644 index 0000000..a87938f --- /dev/null +++ b/changelogs/fragments/37_sap_control_add_system_functions @@ -0,0 +1,3 @@ +minor_changes: + - sap_control - add the mechanisms to handle asynchronous sapcontrol functions + - sap_control - add UpdateSystem, RestartSystem, StartSystem, StopSystem functions From 3cb3cd202d6b5c392e5846befa6bda859cef6f7b Mon Sep 17 00:00:00 2001 From: Nicolas Bettembourg Date: Mon, 21 Jul 2025 13:53:24 +0200 Subject: [PATCH 3/6] Adding *system_sap_nw functions to act only on selected SID Signed-off-by: Nicolas Bettembourg --- roles/sap_control/defaults/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roles/sap_control/defaults/main.yml b/roles/sap_control/defaults/main.yml index 7f4d49b..43ee57d 100644 --- a/roles/sap_control/defaults/main.yml +++ b/roles/sap_control/defaults/main.yml @@ -61,6 +61,10 @@ sap_control_functions_list: - updatesystem_all_nw - startsystem_all_nw - stopsystem_all_nw + - restartsystem_sap_nw + - updatesystem_sap_nw + - startsystem_sap_nw + - stopsystem_sap_nw - restart_all_sap - stop_all_sap - start_all_sap From 57b6a3d8c712694772b3d23da951d2921d21632f Mon Sep 17 00:00:00 2001 From: Nicolas Bettembourg Date: Tue, 22 Jul 2025 10:51:41 +0200 Subject: [PATCH 4/6] Removing failed_when: false from async polling Extending timeout Signed-off-by: Nicolas Bettembourg --- roles/sap_control/tasks/sapcontrol_async.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/roles/sap_control/tasks/sapcontrol_async.yml b/roles/sap_control/tasks/sapcontrol_async.yml index ebe65bc..9fad97c 100644 --- a/roles/sap_control/tasks/sapcontrol_async.yml +++ b/roles/sap_control/tasks/sapcontrol_async.yml @@ -1,7 +1,7 @@ --- - name: Pause for 5 Seconds ansible.builtin.wait_for: - timeout: 5 + timeout: 15 - name: SAP {{ sap_control_name_header }} - Checking if Async action is over by executing sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} ansible.builtin.shell: | @@ -11,7 +11,6 @@ become: true become_user: "{{ passed_sap_sid | lower }}adm" register: test_function_result -# failed_when: "'FAIL' in test_function_result.stdout" retries: "{{ async_function_dict.retries | default(0) | int }}" delay: "{{ async_function_dict.delay | default(0) | int }}" until: > @@ -19,7 +18,6 @@ or async_function_dict.until_false is defined and not test_function_result.stdout | regex_search(async_function_dict.until_false, multiline=True)) and (async_function_dict.until_true is not defined or async_function_dict.until_true is defined and test_function_result.stdout | regex_search(async_function_dict.until_true, multiline=True)) - failed_when: false - name: Debug stdout ansible.builtin.debug: From 89ba23eca3b9bf75887c02cb6f1b903e547992d5 Mon Sep 17 00:00:00 2001 From: Nicolas Bettembourg Date: Wed, 23 Jul 2025 10:15:06 +0200 Subject: [PATCH 5/6] Signed-off-by: Nicolas Bettembourg --- roles/sap_control/tasks/sapcontrol_async.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/sap_control/tasks/sapcontrol_async.yml b/roles/sap_control/tasks/sapcontrol_async.yml index 9fad97c..7272073 100644 --- a/roles/sap_control/tasks/sapcontrol_async.yml +++ b/roles/sap_control/tasks/sapcontrol_async.yml @@ -1,7 +1,7 @@ --- -- name: Pause for 5 Seconds +- name: Pause for 20 Seconds to ensure the async function is started ansible.builtin.wait_for: - timeout: 15 + timeout: 20 - name: SAP {{ sap_control_name_header }} - Checking if Async action is over by executing sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} ansible.builtin.shell: | From 577b25c64c548ed3f8f19ee49ae0672b6ded7f77 Mon Sep 17 00:00:00 2001 From: nbttmbrg Date: Thu, 11 Sep 2025 12:53:14 +0000 Subject: [PATCH 6/6] * Fix incompatibilities with Ansible 2.19 * Adds a pre-polling task for non-idempotent RestartSystem and UpdateSystem functions --------- Signed-off-by: Nicolas Bettembourg Signed-off-by: nbttmbrg --- roles/sap_control/defaults/main.yml | 12 ++++++++ roles/sap_control/tasks/sapcontrol_async.yml | 32 ++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/roles/sap_control/defaults/main.yml b/roles/sap_control/defaults/main.yml index 43ee57d..5a07dc4 100644 --- a/roles/sap_control/defaults/main.yml +++ b/roles/sap_control/defaults/main.yml @@ -96,6 +96,18 @@ updatesystem_all_nw_list: - sap_control_function_current: "nw_startsystem" - sap_control_function_current: "nw_updatesystem" +restartsystem_sap_nw_list: + - sap_control_function_current: "nw_restartsystem" + +updatesystem_sap_nw_list: + - sap_control_function_current: "nw_updatesystem" + +startsystem_sap_nw_list: + - sap_control_function_current: "nw_startsystem" + +stopsystem_sap_nw_list: + - sap_control_function_current: "nw_stopsystem" + restart_all_sap_list: - sap_control_function_current: "nw_stop" - sap_control_function_current: "hana_stop" diff --git a/roles/sap_control/tasks/sapcontrol_async.yml b/roles/sap_control/tasks/sapcontrol_async.yml index 7272073..a704c18 100644 --- a/roles/sap_control/tasks/sapcontrol_async.yml +++ b/roles/sap_control/tasks/sapcontrol_async.yml @@ -1,4 +1,30 @@ --- +- name: Wait for status to change if using restart or update + when: funct_type is match('restart|update') + block: + - name: SAP {{ sap_control_name_header }} - Getting current system state + ansible.builtin.shell: | + source ~/.profile && sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} + args: + executable: /bin/bash + become: true + become_user: "{{ passed_sap_sid | lower }}adm" + register: initial_test_function_result + + - name: SAP {{ sap_control_name_header }} - Waiting for state to change before polling + ansible.builtin.shell: | + source ~/.profile && sapcontrol -nr {{ passed_sap_nr }} -function {{ async_function_dict.test_function }} + args: + executable: /bin/bash + become: true + become_user: "{{ passed_sap_sid | lower }}adm" + register: wait_for_change_result + retries: "{{ async_function_dict.retries | default(0) | int }}" + delay: "{{ async_function_dict.delay | default(0) | int }}" + until: > + (wait_for_change_result.stdout | regex_findall('GREEN|YELLOW|GRAY|RED', multiline=True) | sort | join(',')) + != (initial_test_function_result.stdout | regex_findall('GREEN|YELLOW|GRAY|RED', multiline=True) | sort | join(',')) + - name: Pause for 20 Seconds to ensure the async function is started ansible.builtin.wait_for: timeout: 20 @@ -15,9 +41,11 @@ delay: "{{ async_function_dict.delay | default(0) | int }}" until: > (async_function_dict.until_false is not defined - or async_function_dict.until_false is defined and not test_function_result.stdout | regex_search(async_function_dict.until_false, multiline=True)) and + or async_function_dict.until_false is defined + and test_function_result.stdout | regex_search(async_function_dict.until_false, multiline=True) is none) and (async_function_dict.until_true is not defined or - async_function_dict.until_true is defined and test_function_result.stdout | regex_search(async_function_dict.until_true, multiline=True)) + async_function_dict.until_true is defined + and test_function_result.stdout | regex_search(async_function_dict.until_true, multiline=True) is not none) - name: Debug stdout ansible.builtin.debug: