This commit is contained in:
martin.fencl
2026-07-29 15:10:09 +02:00
parent 1b11379a49
commit 9b49a0835b
+107 -38
View File
@@ -1,22 +1,51 @@
# check_raid.yml ---
- name: Check Linux MD RAID health on pve1
- name: Check Linux MD RAID health
hosts: pve1_vm hosts: pve1_vm
gather_facts: false gather_facts: false
vars: vars:
# Semaphore Survey Variables are preferred.
# Environment variables are used only as a fallback.
raid_md_raw: >-
{{
RAID_MD
| default(lookup('env', 'RAID_MD'), true)
| default('auto', true)
| string
| trim
| lower
}}
# RAID_MD can be md0, md1, /dev/md0, or auto # RAID_MD can be md0, md1, /dev/md0, or auto
raid_md_raw: "{{ lookup('env', 'RAID_MD') | default('auto', true) | trim }}"
raid_md_device: "{{ raid_md_raw | regex_replace('^/dev/', '') }}" raid_md_device: "{{ raid_md_raw | regex_replace('^/dev/', '') }}"
# Allow check, repair, resync, recovery, or reshape on an otherwise healthy array # Allow a healthy array to be checking, repairing, recovering,
raid_allow_sync: "{{ lookup('env', 'RAID_ALLOW_SYNC') | default('1', true) | int }}" # resyncing, or reshaping
raid_allow_sync: >-
{{
RAID_ALLOW_SYNC
| default(lookup('env', 'RAID_ALLOW_SYNC'), true)
| default('1', true)
| int
}}
# Set to 1 only when a server without MD RAID should be considered healthy # A server without Linux MD RAID is considered unhealthy by default
raid_allow_no_array: "{{ lookup('env', 'RAID_ALLOW_NO_ARRAY') | default('0', true) | int }}" raid_allow_no_array: >-
{{
RAID_ALLOW_NO_ARRAY
| default(lookup('env', 'RAID_ALLOW_NO_ARRAY'), true)
| default('0', true)
| int
}}
# Print raw command data and /proc/mdstat when enabled # Enable raw diagnostic output
raid_debug: "{{ lookup('env', 'DEBUG') | default('0', true) | int }}" raid_debug: >-
{{
DEBUG
| default(lookup('env', 'DEBUG'), true)
| default('0', true)
| int
}}
raid_check_script: | raid_check_script: |
import re import re
@@ -37,7 +66,8 @@
except Exception as exc: except Exception as exc:
print( print(
"ERROR RAID " "ERROR RAID "
f"read_mdstat_failed error={type(exc).__name__}:{exc}" f"read_mdstat_failed="
f"{type(exc).__name__}:{exc}"
) )
sys.exit(2) sys.exit(2)
@@ -45,6 +75,7 @@
r"^(md\d+)\s*:\s*(.*)$", r"^(md\d+)\s*:\s*(.*)$",
re.MULTILINE, re.MULTILINE,
) )
header_matches = list(header_pattern.finditer(mdstat)) header_matches = list(header_pattern.finditer(mdstat))
array_blocks = {} array_blocks = {}
@@ -60,24 +91,29 @@
] ]
def md_sort_key(name): def md_sort_key(name):
try:
return int(name[2:]) return int(name[2:])
except ValueError:
return name
def parse_array(name, block): def parse_array(name, block):
header = block.splitlines()[0] lines = block.splitlines()
header = lines[0]
header_data = header.split(":", 1)[1].strip() header_data = header.split(":", 1)[1].strip()
state_match = re.match( state_match = re.match(
r"^(active|inactive)\b", r"^(active|inactive)\b",
header_data, header_data,
re.IGNORECASE,
) )
state = ( state = (
state_match.group(1) state_match.group(1).lower()
if state_match if state_match
else "unknown" else "unknown"
) )
remaining = ( remaining_header = (
header_data[state_match.end():].strip() header_data[state_match.end():].strip()
if state_match if state_match
else header_data else header_data
@@ -85,23 +121,28 @@
mode = "rw" mode = "rw"
if remaining.startswith("("): # Handle states such as "active (auto-read-only)"
closing_parenthesis = remaining.find(")") if remaining_header.startswith("("):
closing_parenthesis = remaining_header.find(")")
if closing_parenthesis >= 0: if closing_parenthesis >= 0:
mode = remaining[1:closing_parenthesis] mode = remaining_header[
remaining = remaining[ 1:closing_parenthesis
]
remaining_header = remaining_header[
closing_parenthesis + 1: closing_parenthesis + 1:
].strip() ].strip()
level = ( level = (
remaining.split()[0] remaining_header.split()[0].lower()
if remaining if remaining_header
else "unknown" else "unknown"
) )
status_match = re.search( status_match = re.search(
r"\[(\d+)\s*/\s*(\d+)\]\s*\[([U_]+)\]", r"\[(\d+)\s*/\s*(\d+)\]"
r"\s*\[([U_]+)\]",
block, block,
) )
@@ -115,7 +156,13 @@
member_status = status_match.group(3) member_status = status_match.group(3)
operation_match = re.search( operation_match = re.search(
r"\b(resync|recovery|reshape|check|repair)\b", r"\b("
r"resync|"
r"recovery|"
r"reshape|"
r"check|"
r"repair"
r")\b",
block, block,
re.IGNORECASE, re.IGNORECASE,
) )
@@ -127,8 +174,13 @@
) )
progress_match = re.search( progress_match = re.search(
r"(?:resync|recovery|reshape|check|repair)" r"(?:"
r"\s*=\s*([0-9.]+%)", r"resync|"
r"recovery|"
r"reshape|"
r"check|"
r"repair"
r")\s*=\s*([0-9.]+%)",
block, block,
re.IGNORECASE, re.IGNORECASE,
) )
@@ -150,20 +202,25 @@
"operation": operation, "operation": operation,
"progress": progress, "progress": progress,
"faulty_member": bool( "faulty_member": bool(
re.search(r"\(F\)", block) re.search(
r"\(F\)",
block,
re.IGNORECASE,
)
), ),
} }
def format_array(array): def format_array(array):
if array["member_status"] is None: if array["member_status"] is None:
members = "n/a" members = "n/a"
status = "n/a" member_status = "n/a"
else: else:
members = ( members = (
f'{array["active_members"]}/' f'{array["active_members"]}/'
f'{array["total_members"]}' f'{array["total_members"]}'
) )
status = array["member_status"]
member_status = array["member_status"]
return ( return (
f'{array["name"]}' f'{array["name"]}'
@@ -172,7 +229,7 @@
f'mode={array["mode"]},' f'mode={array["mode"]},'
f'level={array["level"]},' f'level={array["level"]},'
f'members={members},' f'members={members},'
f'status={status},' f'status={member_status},'
f'operation={array["operation"]},' f'operation={array["operation"]},'
f'progress={array["progress"]}' f'progress={array["progress"]}'
"}" "}"
@@ -187,7 +244,7 @@
sys.exit(2) sys.exit(2)
available_arrays = sorted( available_arrays = sorted(
array_blocks, array_blocks.keys(),
key=md_sort_key, key=md_sort_key,
) )
@@ -198,20 +255,23 @@
else: else:
print( print(
"ERROR RAID " "ERROR RAID "
f"target_not_found target={target} " f"target_not_found={target} "
f'found={",".join(available_arrays)}' f'found={",".join(available_arrays)}'
) )
sys.exit(2) sys.exit(2)
selected_arrays = [ selected_arrays = [
parse_array(name, array_blocks[name]) parse_array(
name,
array_blocks[name],
)
for name in selected_names for name in selected_names
] ]
failures = [] failures = []
errors = [] errors = []
# RAID0 and linear arrays do not normally contain [UU] status tokens # These RAID levels may not always provide a redundant-member token
tokenless_levels = { tokenless_levels = {
"raid0", "raid0",
"linear", "linear",
@@ -257,7 +317,8 @@
and not allow_sync and not allow_sync
): ):
failures.append( failures.append(
f'{name}:operation={array["operation"]}' f'{name}:operation='
f'{array["operation"]}'
) )
details = " ".join( details = " ".join(
@@ -290,12 +351,15 @@
- (raid_allow_no_array | int) in [0, 1] - (raid_allow_no_array | int) in [0, 1]
- (raid_debug | int) in [0, 1] - (raid_debug | int) in [0, 1]
- >- - >-
(raid_md_device | (
regex_search('^(auto|md[0-9]+)$')) raid_md_device
is not none | regex_search('^(auto|md[0-9]+)$')
) is not none
fail_msg: >- fail_msg: >-
ERROR RAID invalid-configuration. ERROR RAID invalid-configuration.
RAID_MD must be auto, md0, md1, or /dev/md0. RAID_MD must be auto, md0, md1, or /dev/md0.
DEBUG, RAID_ALLOW_SYNC, and RAID_ALLOW_NO_ARRAY
must be 0 or 1.
quiet: true quiet: true
- name: Run Linux MD RAID health check - name: Run Linux MD RAID health check
@@ -329,7 +393,8 @@
{%- if stdout | length > 0 -%} {%- if stdout | length > 0 -%}
{{ stdout }} {{ stdout }}
{%- elif stderr | length > 0 -%} {%- elif stderr | length > 0 -%}
ERROR RAID command_failed rc={{ raid_cmd.rc }} stderr={{ stderr[:500] }} ERROR RAID command_failed rc={{ raid_cmd.rc }}
stderr={{ stderr[:500] }}
{%- else -%} {%- else -%}
ERROR RAID no-output rc={{ raid_cmd.rc }} ERROR RAID no-output rc={{ raid_cmd.rc }}
{%- endif -%} {%- endif -%}
@@ -350,11 +415,15 @@
- /proc/mdstat - /proc/mdstat
register: mdstat_debug register: mdstat_debug
changed_when: false changed_when: false
failed_when: false
when: (raid_debug | int) == 1 when: (raid_debug | int) == 1
- name: Show raw mdstat in debug mode - name: Show raw mdstat in debug mode
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ mdstat_debug.stdout | default('') }}" msg:
rc: "{{ mdstat_debug.rc | default('undefined') }}"
stdout: "{{ mdstat_debug.stdout | default('') }}"
stderr: "{{ mdstat_debug.stderr | default('') }}"
when: (raid_debug | int) == 1 when: (raid_debug | int) == 1
- name: Show successful RAID result - name: Show successful RAID result