diff --git a/src/module_utils/filesystem_collector.py b/src/module_utils/filesystem_collector.py index 99d9a84f..b963cb73 100644 --- a/src/module_utils/filesystem_collector.py +++ b/src/module_utils/filesystem_collector.py @@ -671,12 +671,18 @@ def gather_lvm_groups_info(self, lvm_groups, vg_to_disk_names, azure_disk_data): total_iops += perf_data.get("iops", 0) total_mbps += perf_data.get("mbps", 0) + totalsize = vg_data.get("total_size", "") + lvm_groups_info.append( { "Name": vg_name, "Disks": vg_data.get("disks", 0), "LogicalVolumes": vg_data.get("logical_volumes", 0), - "TotalSize": vg_data.get("total_size", ""), + "TotalSize": ( + totalsize.replace("g", "GiB").replace("t", "TiB") + if totalsize and isinstance(totalsize, str) + else totalsize + ), "TotalIOPS": total_iops, "TotalMBPS": total_mbps, } @@ -704,6 +710,8 @@ def gather_lvm_volumes_info(self, lvm_volumes): try: for lv_name, lv_data in lvm_volumes.items(): + size = lv_data.get("size", "") + lvm_volumes_info.append( { "Name": lv_name, @@ -711,7 +719,11 @@ def gather_lvm_volumes_info(self, lvm_volumes): "LVPath": lv_data.get("path", ""), "DMPath": lv_data.get("dm_path", ""), "Layout": lv_data.get("layout", ""), - "Size": lv_data.get("size", ""), + "Size": ( + size.replace("g", "GiB").replace("t", "TiB") + if size and isinstance(size, str) + else size + ), "StripeSize": lv_data.get("stripe_size", ""), "Stripes": lv_data.get("stripes", ""), } diff --git a/src/playbook_00_configuration_checks.yml b/src/playbook_00_configuration_checks.yml index 3c828415..e48e614d 100644 --- a/src/playbook_00_configuration_checks.yml +++ b/src/playbook_00_configuration_checks.yml @@ -31,7 +31,6 @@ | map(attribute='value') | first }}" - - hosts: "{{ sap_sid | upper }}_SCS: {{ sap_sid | upper }}_ERS: {{ sap_sid | upper }}_DB: @@ -98,6 +97,12 @@ checks_var: "common_sap_checks", results_var: "common_sap_results" } + - { + name: "Networking", + file_name: "network", + checks_var: "networking_checks", + results_var: "networking_results" + } loop_control: loop_var: check_type @@ -264,6 +269,23 @@ groups[sap_sid | upper + '_PAS']|default([]) }}" when: hostvars[item].common_sap_results is defined + - name: "Collect networking check results" + ansible.builtin.set_fact: + all_results: "{{ all_results + hostvars[item].networking_results + | default([]) }}" + execution_metadata: "{{ execution_metadata + [ + {'host': item, + 'check_type': 'networking', + 'metadata': hostvars[item].networking_results_metadata + | default({})}] }}" + loop: "{{ groups[sap_sid | upper + '_SCS']|default([]) + + groups[sap_sid | upper + '_ERS']|default([]) + + groups[sap_sid | upper + '_DB']|default([]) + + groups[sap_sid | upper + '_APP']|default([]) + + groups[sap_sid | upper + '_WEB']|default([]) + + groups[sap_sid | upper + '_PAS']|default([]) }}" + when: hostvars[item].networking_results is defined + - name: "Collect DB (HANA) check results" ansible.builtin.set_fact: all_results: "{{ all_results + hostvars[item].db_hana_results diff --git a/src/roles/configuration_checks/tasks/disks.yml b/src/roles/configuration_checks/tasks/disks.yml index 603afc30..5556eea0 100644 --- a/src/roles/configuration_checks/tasks/disks.yml +++ b/src/roles/configuration_checks/tasks/disks.yml @@ -95,7 +95,7 @@ az disk show --name {{ item }} \ --subscription {{ compute_metadata.json.compute.subscriptionId }} \ --resource-group {{ compute_metadata.json.compute.resourceGroupName }} \ - --query "{name:name, sku:sku.name, size:sizeGb, encryption:encryption.type, iops:diskIOPSReadWrite, mbps:diskMBpsReadWrite, size:diskSizeGB}" --output json + --query "{name:name, sku:sku.name, size:sizeGb, encryption:encryption.type, iops:diskIOPSReadWrite, mbps:diskMBpsReadWrite, size:diskSizeGB, tier:tier}" --output json - name: Debug azure disks data collected when: azure_disks_metadata_results is defined @@ -103,8 +103,46 @@ var: azure_disks_metadata_results verbosity: 1 + - name: Check if any NFS filesystem is mounted + ansible.builtin.set_fact: + has_nfs_mounts: "{{ mount_info.stdout_lines | select('search', '\\snfs[34]?\\s') | list | length > 0 }}" + + - name: Check for ANF usage in mount_info, looking for IP addresses + when: has_nfs_mounts | bool + ansible.builtin.set_fact: + anf_ip_addresses: "{{ mount_info.stdout_lines + | map('split', ' ') + | map('list') + | selectattr('1', 'defined') + | map(attribute='1') + | select('match', '^(\\d{1,3}\\.){3}\\d{1,3}:') + | map('regex_replace', '^((\\d{1,3}\\.){3}\\d{1,3}):.*', '\\1') + | list + | unique }}" + - name: Debug ANF IP addresses found + when: anf_ip_addresses is defined + ansible.builtin.debug: + var: anf_ip_addresses + + - name: Check for AFS usage in mount_info, looking for storage account names + when: has_nfs_mounts | bool + ansible.builtin.set_fact: + afs_storage_accounts: "{{ mount_info.stdout_lines + | map('split', ' ') + | map(attribute='1') + | select('search', '\\.file\\.core\\.windows\\.net:/') + | map('regex_replace', '^.*:/([^/]+)/.*', '\\1') + | list + | unique }}" + + - name: Debug AFS storage account names found + when: afs_storage_accounts is defined + ansible.builtin.debug: + var: afs_storage_accounts + - name: Collect ANF storage data if NFS is used when: + - has_nfs_mounts | bool - NFS_provider is defined - "'ANF' in NFS_provider" - ANF_account_rg is defined @@ -155,29 +193,37 @@ - name: Collect AFS storage data if NFS is used when: + - has_nfs_mounts | bool - NFS_provider is defined - "'AFS' in NFS_provider" + - afs_storage_accounts is defined + - afs_storage_accounts | length > 0 register: afs_storage_metadata_results delegate_to: localhost ansible.builtin.shell: executable: /bin/bash cmd: | set -o pipefail - for sa in $(az storage account list \ - --query "[?kind=='FileStorage'].{rg:resourceGroup,name:name,id:id}" \ - -o tsv | awk '{print $1":"$2":"$3}'); do rg=$(echo $sa | cut -d: -f1); \ - acc=$(echo $sa | cut -d: -f2); sid=$(echo $sa | cut -d: -f3); \ - dns="$acc.file.core.windows.net"; for sh in $(az storage share-rm list --resource-group $rg --storage-account $acc \ - --query "[?enabledProtocols=='NFS'].[name,accessTier,quotaGiB]" -o tsv); \ - do name=$(echo $sh | awk '{print $1}'); tier=$(echo $sh | awk '{print $2}'); \ - quota=$(echo $sh | awk '{print $3}'); \ - peip=$(az network private-endpoint list \ - --query "[?privateLinkServiceConnections[?privateLinkServiceId=='$sid']].customDnsConfigs[].ipAddresses[]" -o tsv); \ - for ip in $peip; do thr=$((100 + ( (quota*4+99)/100 ) + ( (quota*6+99)/100 ) )); \ - iops=$((quota+3000)); \ - if [ $iops -gt 100000 ]; then iops=100000; fi; \ - echo "{\"Type\":\"AFS\",\"Name\":\"$name\",\"Pool\":\"$acc\",\"ServiceLevel\":\"$tier\",\"ThroughputMibps\":$thr,\"ProtocolTypes\":\"NFS4.1\",\"NFSAddressDNS\":\"$dns:/$acc/$name\",\"NFSAddress\":\"$ip:/$acc/$name\",\"QoSType\":\"Manual\",\"IOPS\":$iops,\"Id\":\"$sid\"}"; \ - done; done; done + for acc in {{ afs_storage_accounts | join(' ') }}; do + sa_info=$(az storage account show --name "$acc" --query "{rg:resourceGroup,name:name,id:id}" -o tsv) + rg=$(echo "$sa_info" | awk '{print $1}') + sid=$(echo "$sa_info" | awk '{print $3}') + dns="$acc.file.core.windows.net" + for sh in $(az storage share-rm list --resource-group "$rg" --storage-account "$acc" \ + --query "[?enabledProtocols=='NFS'].[name,accessTier,quotaGiB]" -o tsv); do + name=$(echo "$sh" | awk '{print $1}') + tier=$(echo "$sh" | awk '{print $2}') + quota=$(echo "$sh" | awk '{print $3}') + peip=$(az network private-endpoint list \ + --query "[?privateLinkServiceConnections[?privateLinkServiceId=='$sid']].customDnsConfigs[].ipAddresses[]" -o tsv) + for ip in $peip; do + thr=$((100 + ( (quota*4+99)/100 ) + ( (quota*6+99)/100 ) )) + iops=$((quota+3000)) + if [ $iops -gt 100000 ]; then iops=100000; fi + echo "{\"Type\":\"AFS\",\"Name\":\"$name\",\"Pool\":\"$acc\",\"ServiceLevel\":\"$tier\",\"ThroughputMibps\":$thr,\"ProtocolTypes\":\"NFS4.1\",\"NFSAddressDNS\":\"$dns:/$acc/$name\",\"NFSAddress\":\"$ip:/$acc/$name\",\"QoSType\":\"Manual\",\"IOPS\":$iops,\"Id\":\"$sid\"}" + done + done + done - name: Debug AFS storage data collected when: afs_storage_metadata_results is defined diff --git a/src/roles/configuration_checks/tasks/files/hana.yml b/src/roles/configuration_checks/tasks/files/hana.yml index 16cecb0a..db8ac2c4 100644 --- a/src/roles/configuration_checks/tasks/files/hana.yml +++ b/src/roles/configuration_checks/tasks/files/hana.yml @@ -162,57 +162,59 @@ checks: valid_list: ["reboot", "stonith-action=reboot"] report: *check - - id: "DB-HANA-0006" - name: "Load Balancer timestamps Non-HA" - description: "Timestamp parameter for Non-HA Load Balancers" + - id: "DB-HANA-0004" + name: "sysctl net.core.rmem_max" + description: "SAP HANA sysctl net.core.rmem_max" category: *os_check severity: *high workload: *sap applicability: os_type: [*suse, *redhat] os_version: *all_versions - hardware_type: *vm - storage_type: *all_storage + hardware_type: *all_hardware + storage_type: *premium_storage role: [*db_role] database_type: [*hana] - high_availability: false - high_availability_agent: *cluster_type collector_type: *command collector_args: - command: "/sbin/sysctl net.ipv4.tcp_timestamps -n" + command: "/sbin/sysctl net.core.rmem_max -n" user: *root validator_type: *string validator_args: - expected_output: "1" + expected_output: "2500000" report: *check + references: + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" + sap: "3024346" - - id: "DB-HANA-0007" - name: "Load Balancer timestamps HA" - description: "Timestamp parameter for HA Load Balancers" + - id: "DB-HANA-0005" + name: "sysctl net.core.rmem_max" + description: "SAP HANA sysctl net.core.rmem_max" category: *os_check severity: *high workload: *sap applicability: os_type: [*suse, *redhat] os_version: *all_versions - hardware_type: *vm - storage_type: *all_storage + hardware_type: *all_hardware + storage_type: *anf role: [*db_role] database_type: [*hana] - high_availability: true - high_availability_agent: *cluster_type collector_type: *command collector_args: - command: "/sbin/sysctl net.ipv4.tcp_timestamps -n" + command: "/sbin/sysctl net.core.rmem_max -n" user: *root validator_type: *string validator_args: - expected_output: "0" + expected_output: "16777216" report: *check + references: + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" + sap: "3024346" - - id: "DB-HANA-0008" - name: "sysctl net.core.rmem_max" - description: "SAP HANA sysctl net.core.rmem_max" + - id: "DB-HANA-0006" + name: "sysctl net.core.wmem_max" + description: "SAP HANA sysctl net.core.wmem_max" category: *os_check severity: *high workload: *sap @@ -220,22 +222,22 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *premium_storage role: [*db_role] database_type: [*hana] collector_type: *command collector_args: - command: "/sbin/sysctl net.core.rmem_max -n" + command: "/sbin/sysctl net.core.wmem_max -n" user: *root validator_type: *string validator_args: - expected_output: "16777216" + expected_output: "212992" report: *check references: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0009" + - id: "DB-HANA-0007" name: "sysctl net.core.wmem_max" description: "SAP HANA sysctl net.core.wmem_max" category: *os_check @@ -245,7 +247,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -260,7 +262,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0010" + - id: "DB-HANA-0008" name: "sysctl net.ipv4.tcp_rmem" description: "SAP HANA sysctl net.ipv4.tcp_rmem" category: *os_check @@ -270,7 +272,32 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *premium_storage + role: [*db_role] + database_type: [*hana] + collector_type: *command + collector_args: + command: "/sbin/sysctl net.ipv4.tcp_rmem -n" + user: *root + validator_type: *string + validator_args: + expected_output: "4096 131072 6291456" + report: *check + references: + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" + sap: "3024346" + + - id: "DB-HANA-0009" + name: "sysctl net.ipv4.tcp_rmem" + description: "SAP HANA sysctl net.ipv4.tcp_rmem" + category: *os_check + severity: *high + workload: *sap + applicability: + os_type: [*suse, *redhat] + os_version: *all_versions + hardware_type: *all_hardware + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -285,6 +312,31 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" + - id: "DB-HANA-0010" + name: "sysctl net.ipv4.tcp_wmem" + description: "SAP HANA sysctl net.ipv4.tcp_wmem" + category: *os_check + severity: *high + workload: *sap + applicability: + os_type: [*suse, *redhat] + os_version: *all_versions + hardware_type: *all_hardware + storage_type: *premium_storage + role: [*db_role] + database_type: [*hana] + collector_type: *command + collector_args: + command: "/sbin/sysctl net.ipv4.tcp_wmem -n" + user: *root + validator_type: *string + validator_args: + expected_output: "4096 16384 4194304" + report: *check + references: + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" + sap: "3024346" + - id: "DB-HANA-0011" name: "sysctl net.ipv4.tcp_wmem" description: "SAP HANA sysctl net.ipv4.tcp_wmem" @@ -295,7 +347,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -320,7 +372,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -345,7 +397,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -370,7 +422,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -420,7 +472,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *premium_storage role: [*db_role] database_type: [*hana] collector_type: *command @@ -429,13 +481,38 @@ checks: user: *root validator_type: *string validator_args: - expected_output: "1" + expected_output: "0" report: *check references: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - id: "DB-HANA-0017" + name: "sysctl net.ipv4.tcp_timestamps" + description: "SAP HANA sysctl net.ipv4.tcp_timestamps" + category: *os_check + severity: *high + workload: *sap + applicability: + os_type: [*suse, *redhat] + os_version: *all_versions + hardware_type: *all_hardware + storage_type: *anf + role: [*db_role] + database_type: [*hana] + collector_type: *command + collector_args: + command: "/sbin/sysctl net.ipv4.tcp_timestamps -n" + user: *root + validator_type: *string + validator_args: + expected_output: "1" + report: *check + references: + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" + sap: "3024346" + + - id: "DB-HANA-0018" name: "sysctl net.ipv4.tcp_sack" description: "SAP HANA sysctl net.ipv4.tcp_sack" category: *os_check @@ -445,7 +522,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -460,7 +537,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0018" + - id: "DB-HANA-0019" name: "sysctl net.ipv6.conf.all.disable_ipv6" description: "SAP HANA sysctl net.ipv6.conf.all.disable_ipv6" category: *os_check @@ -470,7 +547,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -485,7 +562,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0019" + - id: "DB-HANA-0020" name: "sysctl net.ipv4.tcp_max_syn_backlog" description: "SAP HANA sysctl net.ipv4.tcp_max_syn_backlog" category: *os_check @@ -495,22 +572,22 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command collector_args: command: "/sbin/sysctl net.ipv4.tcp_max_syn_backlog -n" user: *root - validator_type: *string + validator_type: *range validator_args: - expected_output: "16348" + min: "8192" report: *check references: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0020" + - id: "DB-HANA-0021" name: "sysctl net.ipv4.ip_local_port_range" description: "SAP HANA sysctl net.ipv4.ip_local_port_range" category: *os_check @@ -529,13 +606,13 @@ checks: user: *root validator_type: *string validator_args: - expected_output: "9000 65300" + expected_output: "9000 65499" report: *check references: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0021" + - id: "DB-HANA-0022" name: "sysctl net.ipv4.conf.all.rp_filter" description: "SAP HANA sysctl net.ipv4.conf.all.rp_filter" category: *os_check @@ -545,7 +622,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -560,7 +637,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0022" + - id: "DB-HANA-0023" name: "sysctl sunrpc.tcp_slot_table_entries" description: "SAP HANA sysctl sunrpc.tcp_slot_table_entries" category: *os_check @@ -570,7 +647,7 @@ checks: os_type: [*suse, *redhat] os_version: *all_versions hardware_type: *all_hardware - storage_type: *all_storage + storage_type: *anf role: [*db_role] database_type: [*hana] collector_type: *command @@ -585,7 +662,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0023" + - id: "DB-HANA-0024" name: "sysctl vm.swappiness" description: "SAP HANA sysctl vm.swappiness" category: *os_check @@ -610,7 +687,7 @@ checks: microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/sap-hana-scale-out-standby-netapp-files-suse" sap: "3024346" - - id: "DB-HANA-0024" + - id: "DB-HANA-0025" name: "Red Hat tuned-adm profile" description: "SAP HANA Red Hat tuned-adm profile" category: *sap_check @@ -634,30 +711,6 @@ checks: references: sap: "2777782" - - id: "DB-HANA-0025" - name: "Kernel version higher than 4.12.14-95.37.1" - description: "SAP HANA Backup fails on Azure - SLES 12.4" - category: *sap_check - severity: *warning - workload: *sap - applicability: - os_type: [*suse] - os_version: [*suse_12_4] - hardware_type: *vm - storage_type: *premium_storage - role: [*db_role] - database_type: [*hana] - collector_type: *command - collector_args: - command: "uname -r" - user: *root - validator_type: *string - validator_args: - expected_output: "4.12.14-95.37.1" - report: *check - references: - sap: "2814271" - - id: "DB-HANA-0026" name: "Mellanox TX timeout - CPU soft lockup" description: "set hv_storvsc.storvsc_ringbuffer_size=131072 and hv_storvsc.storvsc_vcpus_per_sub_channel=1024 in kernel boot line" @@ -905,7 +958,7 @@ checks: - id: "DB-HANA-0036" name: "Stripe size for /hana/log" - description: "The stripe size for /hana/log should be 256k (256.00k or 262144 bytes)" + description: "The stripe size for /hana/log should be 64 (64.00k or 65536 bytes)" category: *sap_check severity: *high workload: *sap @@ -923,7 +976,7 @@ checks: mount_point: "/hana/log" validator_type: *string validator_args: - expected: "256.00k" + expected: "64.00k" report: *check references: sap: "2972496" @@ -1284,4 +1337,29 @@ checks: report: *check references: sap: "2972496" - microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/hana-vm-operations-storage" \ No newline at end of file + microsoft: "https://docs.microsoft.com/en-us/azure/virtual-machines/workloads/sap/hana-vm-operations-storage" + + + - id: "DB-HANA-0052" + name: "Kernel version higher than 4.12.14-95.37.1" + description: "SAP HANA Backup fails on Azure - SLES 12.4" + category: *sap_check + severity: *warning + workload: *sap + applicability: + os_type: [*suse] + os_version: [*suse_12_4] + hardware_type: *vm + storage_type: *premium_storage + role: [*db_role] + database_type: [*hana] + collector_type: *command + collector_args: + command: "uname -r" + user: *root + validator_type: *string + validator_args: + expected_output: "4.12.14-95.37.1" + report: *check + references: + sap: "2814271" diff --git a/src/roles/configuration_checks/tasks/files/network.yml b/src/roles/configuration_checks/tasks/files/network.yml new file mode 100644 index 00000000..07cf25d1 --- /dev/null +++ b/src/roles/configuration_checks/tasks/files/network.yml @@ -0,0 +1,220 @@ +enums: + severity: + - info: &info "INFO" + - high: &high "HIGH" + - low: &low "LOW" + - warning: &warning "WARNING" + - critical: &critical "CRITICAL" + - all_severity: &severity [*info, *high, *low, *warning, *critical] + + os_type: + - suse: &suse "SLES_SAP" + - redhat: &redhat "REDHAT" + - oraclelinux: &oraclelinux "OracleLinux" + - windows: &windows "Windows" + - all_os: &os_type [*suse, *redhat, *oraclelinux, *windows] + + os_version: + - suse_12_3: &suse_12_3 "SUSE 12 SP3" + - suse_12_4: &suse_12_4 "SUSE 12 SP4" + - suse_12_5: &suse_12_5 "SUSE 12 SP5" + - suse_15_0: &suse_15_0 "SUSE 15 SP0" + - suse_15_0: &suse_15_1 "SUSE 15 SP1" + - all_versions: &all_versions "all" + + hardware_type: + - vm: &vm "VM" + - hli: &hli "HLI" + - all_hardware: &all_hardware [*vm, *hli] + + storage_type: + premium_storage: &premium_storage ["Premium_LRS","UltraSSD_LRS","PremiumV2_LRS","AFS"] + anf: &anf ["ANF"] + all_storage: &all_storage ["Premium_LRS","UltraSSD_LRS","StandardSSD_LRS","Standard_LRS","ANF","PremiumV2_LRS","AFS"] + + workload: + - sap: &sap "SAP" + - all_workload: &workload [*sap] + + db: + - hana: &hana "HANA" + - mssql: &mssql "MSSQL" + - oracle: &oracle "Oracle" + - db2: &db2 "Db2" + - ase: &ase "ASE" + - all_db: &db [*hana, *mssql, *oracle, *db2, *ase] + + role: + - db: &db_role "DB" + - ascs: &ascs_role "SCS" + - ers: &ers_role "ERS" + - app: &app_role "APP" + - webdispatcher: &web_dispatch "WEB" + - pas: &pas "PAS" + - all_role: &role [*db_role, *ascs_role, *ers_role, *app_role, *web_dispatch, *pas] + + cluster_type: + - sbd: &sbd "ISCSI" + - fencing_agent: &fencing_agent "AFA" + - all_fencing_agent: &cluster_type [*sbd, *fencing_agent] + + collector_type: + - command: &command "command" + - azure: &azure "azure" + - all_collector_type: &collector_type [*command, *azure] + + category: + - package: &package_check "Package" + - vm: &vm_check "Virtual Machine" + - sap: &sap_check "SAP" + - os: &os_check "Operating System" + - network: &network_check "Networking" + - all_check_types: &category [*package_check, *vm_check, *sap_check, *os_check, *network_check] + + user: + - root: &root "root" + - sidadm: &sidadm "sidadm" + - all_users: &user [*root, *sidadm] + + validator_type: + - string: &string "string" + - range: &range "range" + - list: &list "list" + - all: &validator_type [*string, *range, *list] + + report: + - check: &check "check" + - section: §ion "section" + - table: &table "table" + - report: &report [*check, *section, *table] + +checks: + - id: "NET-0001" + name: "No of network interface" + description: "Checks the number of network interfaces on the VM" + category: *network_check + severity: *info + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].{Name:id}" -o tsv | wc -l + report: *check + + - id: "NET-0002" + name: "Network Interface Name" + description: "Retrieves the name of the network interface(s) attached to the VM" + category: *network_check + severity: *info + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].id" -o tsv | xargs -I {} basename {} + report: *check + + - id: "NET-0003" + name: "Subnet" + description: "Retrieves the subnet(s) associated with the VM's network interface(s)" + category: *network_check + severity: *info + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].id" -o tsv | while read nic_id; do \ + nic=$(basename "$nic_id"); \ + az network nic show --resource-group {{ CONTEXT.resource_group_name }} --name "$nic" \ + --query "ipConfigurations[].subnet.id" -o tsv | xargs -I {} basename {}; \ + done + report: *check + + - id: "NET-0004" + name: "Accelerated Networking" + description: "Checks if Accelerated Networking is enabled on the VM's network interface(s)" + category: *network_check + severity: *high + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].id" -o tsv | while read nic_id; do \ + nic=$(basename "$nic_id"); \ + status=$(az network nic show --resource-group {{ CONTEXT.resource_group_name }} --name "$nic" \ + --query "enableAcceleratedNetworking" -o tsv); \ + echo "$status"; \ + done + validator_type: *string + validator_args: + expected_output: "true" + report: *check + + - id: "NET-0005" + name: "No of IP configurations" + description: "Checks the number of IP configurations on each network interface" + category: *network_check + severity: *info + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].id" -o tsv | while read nic_id; do \ + nic=$(basename "$nic_id"); \ + count=$(az network nic show --resource-group {{ CONTEXT.resource_group_name }} --name "$nic" \ + --query "ipConfigurations | length(@)" -o tsv); \ + echo "$nic: $count"; \ + done + report: *check + + - id: "NET-0006" + name: "IP Details" + description: "Retrieves all IP addresses configured on the VM's network interface(s)" + category: *network_check + severity: *info + workload: *workload + applicability: + hardware_type: *vm + collector_type: *azure + collector_args: + command: |- + az vm nic list --resource-group {{ CONTEXT.resource_group_name }} \ + --vm-name {{ CONTEXT.vm_name }} \ + --subscription {{ CONTEXT.subscription_id }} \ + --query "[].id" -o tsv | while read nic_id; do \ + nic=$(basename "$nic_id"); \ + az network nic show --resource-group {{ CONTEXT.resource_group_name }} --name "$nic" \ + --query "ipConfigurations[].{IP:privateIPAddress, Primary:primary}" -o tsv | \ + while IFS=$'\t' read -r ip is_primary; do \ + if [ "${is_primary,,}" = "true" ]; then \ + echo "$nic: $ip Primary"; \ + else \ + echo "$nic: $ip Secondary"; \ + fi; \ + done; \ + done + report: *check diff --git a/src/roles/configuration_checks/tasks/files/virtual_machine.yml b/src/roles/configuration_checks/tasks/files/virtual_machine.yml index 671517a3..cbfb01b0 100644 --- a/src/roles/configuration_checks/tasks/files/virtual_machine.yml +++ b/src/roles/configuration_checks/tasks/files/virtual_machine.yml @@ -259,26 +259,23 @@ checks: report: *check - id: "IC-0012" - name: "OS Timezone" - description: "Checks the OS timezone" + name: "Linux Major and Minor Release" + description: "Checks the Linux major and minor release" category: *vm_check severity: *info workload: *workload applicability: - os_type: [*suse, *redhat, *oraclelinux] - os_version: *all_versions + os_type: [*suse] hardware_type: *vm - role: *role - database_type: *db collector_type: *command collector_args: - command: "/bin/date +%Z" + command: "/bin/cat /etc/os-release | grep VARIANT_ID | cut -d '=' -f2 | tr -d '\"'" user: *root report: *check - id: "IC-0013" - name: "OS KDUMP Configuration" - description: "Checks the KDUMP configuration of the system" + name: "OS Timezone" + description: "Checks the OS timezone" category: *vm_check severity: *info workload: *workload @@ -290,7 +287,7 @@ checks: database_type: *db collector_type: *command collector_args: - command: "/bin/systemctl status kdump.service | grep -o 'Active:.*'" + command: "/bin/date +%Z" user: *root report: *check @@ -661,7 +658,7 @@ checks: if [ -z "$ppg_id" ] || [ "$ppg_id" = "null" ]; then echo "No PPG defined"; else - az ppg show --ids "$ppg_id" --query 'virtualMachines[].id' -o tsv 2>/dev/null || echo "Error: Failed to retrieve PPG VMs"; + az ppg show --ids "$ppg_id" --query 'virtualMachines[].id' -o tsv 2>/dev/null | while read vm_id; do basename "$vm_id"; done || echo "Error: Failed to retrieve PPG VMs"; fi else echo "Error: Failed to retrieve VM PPG"; @@ -883,7 +880,6 @@ checks: fi report: *check - - id: "IC-0040" name: "Cluster Configuration" description: "Checks the cluster configuration of the system" @@ -903,3 +899,21 @@ checks: command: "crm config show" user: *root report: *section + + - id: "IC-0041" + name: "OS KDUMP Configuration" + description: "Checks the KDUMP configuration of the system" + category: *vm_check + severity: *info + workload: *workload + applicability: + os_type: [*suse, *redhat, *oraclelinux] + os_version: *all_versions + hardware_type: *vm + role: *role + database_type: *db + collector_type: *command + collector_args: + command: "/bin/systemctl status kdump.service | grep -o 'Active:.*'" + user: *root + report: *check \ No newline at end of file diff --git a/src/roles/configuration_checks/tasks/main.yml b/src/roles/configuration_checks/tasks/main.yml index be9623fb..79c5a7d9 100644 --- a/src/roles/configuration_checks/tasks/main.yml +++ b/src/roles/configuration_checks/tasks/main.yml @@ -55,6 +55,7 @@ check_type: "{{ check_type }}" vm_name: "{{ compute_metadata.json.compute.name }}" resource_group_name: "{{ compute_metadata.json.compute.resourceGroupName }}" + subscription_id: "{{ compute_metadata.json.compute.subscriptionId | default('unknown') }}" supported_configurations: "{{ vm_support }}" hostname: "{{ inventory_hostname }}" os_type: "{{ ansible_distribution | upper }}" diff --git a/src/templates/config_checks_report.html b/src/templates/config_checks_report.html index 62bbe2c7..b9c029c3 100644 --- a/src/templates/config_checks_report.html +++ b/src/templates/config_checks_report.html @@ -7,9 +7,12 @@ {% macro render_table(data, is_nested=false) %} - {% if not data %} -
No data available
+ {% if not data or (data is iterable and data is not string and data is not mapping and data|length == 0) %} + {# Don't render anything for empty data #} {% elif data is mapping %} + {% if data|length == 0 %} + {# Don't render anything for empty dict #} + {% else %} {% set has_nested = namespace(value=false) %} {% for value in data.values() %} {% if value is mapping or value is iterable and value is not string %} @@ -41,6 +44,7 @@ {% endif %} + {% endif %} {% elif data is iterable and data is not string %} {% if data|length > 0 and data[0] is mapping %} {% set first_item = data[0] %} @@ -1199,6 +1203,8 @@

{{ check_type }}

{% set type_regular_checks = type_checks|selectattr('check.report', 'equalto', 'check')|list %} {% if type_regular_checks|length > 0 %} + {# Sort checks alphabetically by ID #} + {% set sorted_checks = type_regular_checks|sort(attribute='check.id') %} @@ -1210,7 +1216,7 @@

{{ check_type }}

- {% for check in type_regular_checks %} + {% for check in sorted_checks %} @@ -1303,6 +1309,8 @@

References

{% set type_table_checks = type_checks|selectattr('check.report', 'equalto', 'table')|list %} {% if type_table_checks|length > 0 %} {% for check in type_table_checks %} + {% set has_data = check.actual_value and ((check.actual_value is mapping and check.actual_value|length > 0) or (check.actual_value is iterable and check.actual_value is not string and check.actual_value|length > 0) or (check.actual_value is not mapping and check.actual_value is not iterable)) %} + {% if has_data %}

{{ check.check.name }} ({{ check.check.id }})

{{ check.check.description }}

@@ -1315,6 +1323,7 @@

{{ check.check.name }} ({{ check.check.id }})

{{ render_table(check.actual_value) }}
+ {% endif %} {% endfor %} {% endif %} diff --git a/tests/module_utils/collector_test.py b/tests/module_utils/collector_test.py new file mode 100644 index 00000000..99a6ee83 --- /dev/null +++ b/tests/module_utils/collector_test.py @@ -0,0 +1,526 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Unit tests for the collector module. +""" + +import json +from typing import Any, Dict +from unittest.mock import Mock + +import pytest + +from src.module_utils.collector import ( + AzureDataParser, + Collector, + CommandCollector, + ModuleCollector, +) + + +class MockCheck: + """ + Mock Check object for testing + """ + + def __init__(self, collector_args: Dict[str, Any] | None = None): + self.collector_args = collector_args or {} + self.command = None + + +class MockParent: + """ + Mock SapAutomationQA parent for testing + """ + + def __init__(self): + self.logs = [] + self.errors = [] + + def log(self, level: int, message: str) -> None: + """ + Mock log method + """ + self.logs.append({"level": level, "message": message}) + + def handle_error(self, error: Exception) -> None: + """ + Mock handle_error method + """ + self.errors.append(error) + + def execute_command_subprocess(self, command: str, shell_command: bool = True) -> str: + """ + Mock execute_command_subprocess method + """ + return "mock_output" + + +class TestCollector: + """ + Test suite for Collector base class + """ + + def test_collector_abstract_collect(self): + """ + Test that Collector is abstract and cannot be instantiated directly + """ + parent = MockParent() + with pytest.raises(TypeError, match="abstract"): + _ = Collector(parent) # type: ignore + + def test_sanitize_command_valid_and_dangerous(self): + """ + Test sanitize_command with valid and dangerous patterns + """ + collector = CommandCollector(MockParent()) + assert collector.sanitize_command("ls -la") == "ls -la" + assert collector.sanitize_command("ps aux | grep java") == "ps aux | grep java" + with pytest.raises(ValueError, match="dangerous pattern"): + collector.sanitize_command("sudo rm -rf /var/log") + with pytest.raises(ValueError, match="maximum length"): + collector.sanitize_command("a" * 3001) + + def test_substitute_context_vars(self): + """ + Test context variable substitution in commands + """ + collector = CommandCollector(MockParent()) + result = collector.substitute_context_vars( + "ssh {{ CONTEXT.user }}@{{ CONTEXT.host }}", {"user": "admin", "host": "192.168.1.1"} + ) + assert result == "ssh admin@192.168.1.1" + result = collector.substitute_context_vars( + "echo {{ CONTEXT.exists }} {{ CONTEXT.missing }}", {"exists": "value"} + ) + assert result == "echo value {{ CONTEXT.missing }}" + + +class TestCommandCollector: + """ + Test suite for CommandCollector + """ + + def test_collect_success_with_substitution(self): + """ + Test successful command collection with context substitution + """ + collector = CommandCollector(MockParent()) + check = MockCheck({"command": "cat {{ CONTEXT.file }}", "shell": True}) + context = {"file": "/etc/hosts"} + result = collector.collect(check, context) + assert result == "mock_output" + assert check.command == "cat /etc/hosts" + + def test_collect_error_cases(self): + """ + Test command collection error paths + """ + collector = CommandCollector(MockParent()) + assert "ERROR: No command specified" in collector.collect(MockCheck({}), {}) + check = MockCheck({"command": "rm -rf /tmp"}) + assert "ERROR: Command sanitization failed" in collector.collect(check, {}) + check = MockCheck({"command": "{{ CONTEXT.cmd }} -rf /tmp"}) + assert "ERROR: Command sanitization failed after substitution" in collector.collect( + check, {"cmd": "rm"} + ) + check = MockCheck({"command": "whoami", "user": "user;malicious"}) + assert "ERROR: Invalid user parameter" in collector.collect(check, {}) + + def test_collect_user_handling(self): + """ + Test user parameter handling in command collection + """ + collector = CommandCollector(MockParent()) + check = MockCheck({"command": "whoami", "user": "sapuser", "shell": True}) + result = collector.collect(check, {}) + assert result == "mock_output" + check = MockCheck({"command": "ls /root", "user": "root", "shell": True}) + result = collector.collect(check, {}) + assert check.command == "ls /root" + + def test_collect_exception_handling(self, monkeypatch): + """ + Test collect handles exceptions properly + """ + parent = MockParent() + collector = CommandCollector(parent) + + def mock_execute_failing(command: str, shell_command: bool = True) -> str: + raise Exception("Command failed") + + monkeypatch.setattr(parent, "execute_command_subprocess", mock_execute_failing) + result = collector.collect(MockCheck({"command": "failing_command", "shell": True}), {}) + assert "ERROR: Command execution failed" in result + assert len(parent.errors) == 1 + + +class TestAzureDataParser: + """ + Test suite for AzureDataParser + """ + + def test_parse_azure_disks_vars(self): + """ + Test parsing Azure disks variables + """ + parser = AzureDataParser(MockParent()) + assert ( + parser.parse_azure_disks_vars(MockCheck({}), {"azure_disks_info": "disk_data"}) + == "disk_data" + ) + assert parser.parse_azure_disks_vars(MockCheck({}), {}) == "N/A" + + assert ( + parser.parse_anf_volumes_vars(MockCheck({}), {"anf_volumes_info": "anf_volume_data"}) + == "anf_volume_data" + ) + assert ( + parser.parse_lvm_groups_vars(MockCheck({}), {"lvm_groups_info": "lvm_groups_data"}) + == "lvm_groups_data" + ) + assert ( + parser.parse_lvm_volumes_vars(MockCheck({}), {"lvm_volumes_info": "lvm_volumes_data"}) + == "lvm_volumes_data" + ) + assert ( + parser.parse_filesystem_vars( + MockCheck({}), {"formatted_filesystem_info": "filesystem_data"} + ) + == "filesystem_data" + ) + + def test_parse_anf_vars_success(self): + """ + Test successful ANF property parsing with JSON string metadata + """ + parser = AzureDataParser(MockParent()) + + anf_volume = {"ip": "10.0.0.1", "throughput": "1000"} + filesystem = {"target": "/hana/shared", "source": "10.0.0.1:/volume1", "nfs_type": "ANF"} + check = MockCheck({"mount_point": "/hana/shared", "property": "throughput"}) + context = {"filesystems": [filesystem], "anf_storage_metadata": [anf_volume]} + assert parser.parse_anf_vars(check, context) == "1000" + context["anf_storage_metadata"] = json.dumps([anf_volume]) + assert parser.parse_anf_vars(check, context) == "1000" + + def test_parse_anf_vars_mount_not_found(self): + """ + Test ANF parsing when mount point not found + """ + parser = AzureDataParser(MockParent()) + check = MockCheck({"mount_point": "/missing", "property": "throughput"}) + assert ( + parser.parse_anf_vars(check, {"filesystems": [], "anf_storage_metadata": []}) == "N/A" + ) + + filesystem = {"target": "/hana/shared", "source": "/dev/sdb1", "nfs_type": "NFS"} + check = MockCheck({"mount_point": "/hana/shared", "property": "throughput"}) + + assert ( + parser.parse_anf_vars(check, {"filesystems": [filesystem], "anf_storage_metadata": []}) + == "N/A" + ) + filesystem = {"target": "/hana/shared", "source": "invalid_source", "nfs_type": "ANF"} + check = MockCheck({"mount_point": "/hana/shared", "property": "throughput"}) + + assert ( + parser.parse_anf_vars(check, {"filesystems": [filesystem], "anf_storage_metadata": []}) + == "N/A" + ) + + def test_parse_anf_vars_ip_not_found(self): + """ + Test ANF parsing when IP not found in metadata + """ + parser = AzureDataParser(MockParent()) + anf_volume = {"ip": "10.0.0.2", "throughput": "1000"} + filesystem = {"target": "/hana/shared", "source": "10.0.0.1:/volume1", "nfs_type": "ANF"} + assert ( + parser.parse_anf_vars( + MockCheck({"mount_point": "/hana/shared", "property": "throughput"}), + {"filesystems": [filesystem], "anf_storage_metadata": [anf_volume]}, + ) + == "N/A" + ) + + anf_volume = {"ip": "10.0.0.1", "size": "4096"} + filesystem = {"target": "/hana/shared", "source": "10.0.0.1:/volume1", "nfs_type": "ANF"} + + assert ( + parser.parse_anf_vars( + MockCheck({"mount_point": "/hana/shared", "property": "throughput"}), + {"filesystems": [filesystem], "anf_storage_metadata": [anf_volume]}, + ) + == "N/A" + ) + + def test_parse_anf_vars_invalid_json(self): + """ + Test ANF parsing with invalid JSON string + """ + parser = AzureDataParser(MockParent()) + filesystem = {"target": "/hana/shared", "source": "10.0.0.1:/volume1", "nfs_type": "ANF"} + + assert ( + parser.parse_anf_vars( + MockCheck({"mount_point": "/hana/shared", "property": "throughput"}), + {"filesystems": [filesystem], "anf_storage_metadata": "invalid json {"}, + ) + == "N/A" + ) + + filesystem = {"target": "/hana/shared", "source": "10.0.0.1:/volume1", "nfs_type": "ANF"} + + assert ( + parser.parse_anf_vars( + MockCheck({"mount_point": "/hana/shared", "property": "throughput"}), + {"filesystems": [filesystem], "anf_storage_metadata": 12345}, + ) + == "N/A" + ) + result = parser.parse_anf_vars( + MockCheck({"mount_point": "/hana/shared", "property": "throughput"}), + {"filesystems": None, "anf_storage_metadata": []}, + ) + assert "ERROR: ANF property parsing failed" in result + + def test_parse_disks_vars_property_in_filesystem(self): + """ + Test disk parsing when property exists in filesystem entry + """ + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/data", "property": "iops"}), + { + "filesystems": [{"target": "/hana/data", "iops": "5000"}], + "azure_disks_metadata": [], + }, + ) + == "5000" + ) + + def test_parse_disks_vars_lvm_aggregation(self): + """ + Test disk parsing with LVM striped volume aggregation and JSON strings + """ + parser = AzureDataParser(MockParent()) + + disk1 = {"name": "disk1", "iops": 2000} + disk2 = {"name": "disk2", "iops": 2000} + filesystem = {"target": "/hana/data", "azure_disk_names": ["disk1", "disk2"]} + check = MockCheck({"mount_point": "/hana/data", "property": "iops"}) + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk1, disk2]} + ) + == "4000" + ) + disk1_json = '{"name": "disk1", "iops": 1500}' + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk1_json, disk2]} + ) + == "3500" + ) + + def test_parse_disks_vars_single_disk(self): + """ + Test disk parsing for single disk with device name matching + """ + parser = AzureDataParser(MockParent()) + disk = {"name": "/dev/sdc", "iops": 3000} + filesystem = {"target": "/hana/log", "source": "/dev/sdc"} + check = MockCheck({"mount_point": "/hana/log", "property": "iops"}) + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk]} + ) + == "3000" + ) + disk_fallback = {"name": "sdc", "iops": 3000} + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk_fallback]} + ) + == "3000" + ) + + def test_parse_disks_vars_mount_not_found(self): + """ + Test disk parsing when mount point not found + """ + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/missing", "property": "iops"}), + {"filesystems": [], "azure_disks_metadata": []}, + ) + == "N/A" + ) + + def test_parse_disks_vars_no_disk_metadata(self): + """ + Test disk parsing with no disk metadata or not found + """ + parser = AzureDataParser(MockParent()) + filesystem = {"target": "/hana/data", "source": "/dev/sdc"} + check = MockCheck({"mount_point": "/hana/data", "property": "iops"}) + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": []} + ) + == "N/A" + ) + disk = {"name": "other_disk", "iops": 3000} + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk]} + ) + == "N/A" + ) + + def test_parse_disks_vars_property_not_found(self): + """ + Test disk parsing when property not in disk metadata + """ + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/log", "property": "iops"}), + { + "filesystems": [{"target": "/hana/log", "source": "/dev/sdc"}], + "azure_disks_metadata": [{"name": "/dev/sdc", "size": "512"}], + }, + ) + == "N/A" + ) + + def test_parse_disks_vars_invalid_disk_value(self): + """ + Test disk parsing with non-numeric disk values + """ + parser = AzureDataParser(MockParent()) + disk1 = {"name": "disk1", "iops": "invalid"} + disk2 = {"name": "disk2", "iops": 2000} + filesystem = {"target": "/hana/data", "azure_disk_names": ["disk1", "disk2"]} + check = MockCheck({"mount_point": "/hana/data", "property": "iops"}) + assert ( + parser.parse_disks_vars( + check, {"filesystems": [filesystem], "azure_disks_metadata": [disk1, disk2]} + ) + == "2000" + ) + + def test_parse_disks_vars_no_matching_disks(self): + """ + Test disk parsing when no disks match + """ + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/data", "property": "iops"}), + { + "filesystems": [ + {"target": "/hana/data", "azure_disk_names": ["disk1", "disk2"]} + ], + "azure_disks_metadata": [{"name": "other_disk", "iops": 2000}], + }, + ) + == "N/A" + ) + + # Test disk parsing with invalid JSON string in metadata + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/data", "property": "iops"}), + { + "filesystems": [{"target": "/hana/data", "azure_disk_names": ["disk1"]}], + "azure_disks_metadata": ["invalid json {"], + }, + ) + == "N/A" + ) + + # Test disk parsing with unexpected metadata type + assert ( + AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/data", "property": "iops"}), + { + "filesystems": [{"target": "/hana/data", "azure_disk_names": ["disk1"]}], + "azure_disks_metadata": [12345], + }, + ) + == "N/A" + ) + + # Test disk parsing handles exceptions + result = AzureDataParser(MockParent()).parse_disks_vars( + MockCheck({"mount_point": "/hana/data", "property": "iops"}), + {"filesystems": None, "azure_disks_metadata": []}, + ) + assert "ERROR: Parsing failed" in result + + # Test AzureDataParser.collect delegates to CommandCollector + result = AzureDataParser(MockParent()).collect( + MockCheck({"command": "echo test", "shell": True}), {} + ) + assert result == "mock_output" + + +class TestModuleCollector: + """ + Test suite for ModuleCollector + """ + + def test_collect_with_context_key(self): + """ + Test collecting module data with explicit context_key + """ + result = ModuleCollector(MockParent()).collect( + MockCheck({"module_name": "test_module", "context_key": "test_data"}), + {"test_data": {"result": "success"}}, + ) + assert result == {"result": "success"} + + # Test collecting module data using default context key mapping + result = ModuleCollector(MockParent()).collect( + MockCheck({"module_name": "get_pcmk_properties_db"}), + {"ha_db_config": {"config": "value"}}, + ) + assert result == {"config": "value"} + + # Test collecting module data for unmapped module + + result = ModuleCollector(MockParent()).collect( + MockCheck({"module_name": "custom_module"}), {"custom_module": {"data": "value"}} + ) + assert result == {"data": "value"} + + def test_collect_no_module_name(self): + """ + Test collect with no module_name specified + """ + assert "ERROR: No module_name specified" in ModuleCollector(MockParent()).collect( + MockCheck({}), {} + ) + + result = ModuleCollector(MockParent()).collect( + MockCheck({"module_name": "test_module", "context_key": "missing_key"}), + {"other_key": "value"}, + ) + assert "ERROR: Module result 'missing_key' not found in context" in result + + def test_collect_all_mapped_modules(self): + """ + Test collect with all pre-mapped module names + """ + mappings = { + "get_pcmk_properties_db": "ha_db_config", + "get_pcmk_properties_scs": "ha_scs_config", + "get_azure_lb": "ha_loadbalancer_config", + } + + for module_name, context_key in mappings.items(): + result = ModuleCollector(MockParent()).collect( + MockCheck({"module_name": module_name}), {context_key: {"test": "data"}} + ) + assert result == {"test": "data"}
{{ check.check.id }} {{ check.check.name }}