| name | sap-config-validator |
| description | Validates SAP system configurations against Microsoft's SAP Testing Automation Framework (STAF). Pulls STAF check definitions live from the public Azure/sap-automation-qa GitHub repo, reads collected VM configs from the customer's sap-configs blob container, and runs the comparison entirely in-skill. Requires a Storage Account in Deployed Infrastructure. Read-only. |
| tools | ["ExecutePythonCode","RunAzCliReadCommands"] |
When to Use
- "Run config checks for AB1" / "Validate configuration for AB3"
- "STAF checks for HSO" / "Run all configuration checks"
- "Check OS parameters" / "Check HANA configuration"
Topology handling (all 8 system types)
Derive HA_TYPE from architecture + deployment (see the values block below) and iterate nodes by topology:
- scale-out → validate every DB node (master + workers + standby) and compare
sysctl / global.ini across all worker nodes — they must be uniform for HSR partitioning; flag any drift.
- standalone / distributed →
HA_TYPE = "false"; skip the high_availability STAF checks.
- high-availability / disaster-recovery →
HA_TYPE = scale_up|scale_out; run the HA checks. For disaster-recovery, also confirm the DR-region nodes have collected configs and validate them too.
Infrastructure Requirements
This skill requires a Storage Account in the ## Deployed Infrastructure section of Team Onboarding.
- If no Storage Account is listed — Respond exactly: "Config validation requires a Storage Account with collected SAP configs (the
sap-configs container must exist and the agent MI must have Storage Blob Data Reader on it). No Storage Account is listed in Deployed Infrastructure. Run infra/deploy-sre-infra.ps1 to deploy one." Then stop. Do NOT attempt to fetch STAF or list blobs.
- If Storage Account is listed — Run the flow below. Configs are read from blob using the SRE Agent's own Managed Identity (built into
RunAzCliReadCommands — --auth-mode login). No proxy is needed for this skill.
Architecture
STAF check definitions Collected configs
(Azure/sap-automation-qa @ main) (Azure Blob Storage)
│ │
│ requests.get │ az storage blob (--auth-mode login)
│ (9 YAML files) │ uses SRE Agent UMI
▼ ▼
└────────► ExecutePythonCode ◄─────┘
│
│ parse YAML → filter applicability →
│ extract actuals from collected files →
│ compare against expected_output/valid_list/min/max
▼
compliance report
Rules
- Never invent or fabricate checks. Every check ID, expected value, and reference must come from the STAF YAML fetched from GitHub. If GitHub is unreachable and no checks load, report the failure and stop.
- Never deploy anything. No collector deployment, no VM commands, no infrastructure changes.
- Never use
az vm run-command. If configs are missing or stale, instruct the user to re-run the collector on the affected VM (via az vm run-command or their config-mgmt tool) so fresh configs land in the sap-configs blob.
- Present results exactly as computed — do not add, remove, or modify any check result.
STAF YAML schema (verified against Azure/sap-automation-qa @ main)
Each YAML file has a top-level checks: list. Each check has:
id, name, description, category, severity
applicability — filter keys: os_type, os_version, role (singular: DB, SCS, ERS, APP, WEB, PAS), database_type, storage_type, workload, hardware_type, high_availability (bool, or list of scale_up/scale_out), high_availability_agent (ISCSI or AFA)
collector_type — command (shell command run on the VM) or azure (ARM API lookup — not supported by this skill, returns not_evaluated)
collector_args — for command: command: (shell string) and user: (root or sidadm). For azure: resource_type, property, optional mount_point
validator_type — string, range, or list
validator_args — expected_output (string), valid_list (list), min/max (range)
report — check (compare actual vs expected — the only kind we evaluate), section (UI heading), or table (data-only)
references — list of SAP Note / Microsoft docs URLs
Execution
Step 1 — Download collected configs from blob
Use RunAzCliReadCommands. The values for <storage>, <SID>, and <host> come from the Team Onboarding ## Deployed Infrastructure and ## SAP Landscape sections. Authentication is the agent's own Managed Identity (--auth-mode login) — no keys, no SAS.
az storage blob list \
--account-name <storage> --container-name sap-configs \
--prefix "<SID>/<host>/latest/" --auth-mode login \
--query "[].{name:name, modified:properties.lastModified}" -o json
az storage blob download-batch \
--source sap-configs --pattern "<SID>/<host>/latest/*" \
--destination /tmp/configs/<SID>/<host>/ \
--account-name <storage> --auth-mode login
If the blob list is empty or the newest file is older than 14 days, stop and report:
"No fresh collected configs found for <SID>/<host> in <storage>/sap-configs. The collector may not be installed on this VM. Re-run the collector on the VM (az vm run-command invoke -g <RG> -n <vm> --command-id RunShellScript --scripts 'sudo /opt/sre/collect-sap-configs.sh', or your config-management tool) so fresh configs land in the sap-configs blob, then re-run this validation."
Step 2 — Fetch STAF definitions and run the comparison
Use ExecutePythonCode. Set the six landscape variables at the top from the Team Onboarding inventory before running.
import json, re, requests, yaml
from pathlib import Path
SID = "AB1"
HOST = "AB1vm"
OS_TYPE = "SLES_SAP"
ROLES = ["DB", "SCS", "PAS"]
DB_TYPE = "HANA"
STORAGE_TYPE = "Premium_LRS"
HA_TYPE = "false"
HA_AGENT = "none"
CONFIG_DIR = Path(f"/tmp/configs/{SID}/{HOST}/latest")
STAF_FILES = ["hana.yml", "sap.yml", "virtual_machine.yml", "network.yml",
"ascs.yml", "app.yml", , , ]
STAF_BASE = (
)
():
:
yaml.safe_load(text)
yaml.YAMLError:
m = re.search(, text, re.MULTILINE)
m:
pos = text.rfind(, , m.start()) +
yaml.safe_load(text[pos:] + + text[:pos])
all_checks, fetch_errors = [], []
fname STAF_FILES:
:
r = requests.get(, timeout=)
r.status_code != :
fetch_errors.append();
parsed = parse_yaml(r.text)
parsed parsed:
chk parsed[]:
chk[] = fname
all_checks.append(chk)
Exception e:
fetch_errors.append()
all_checks:
(json.dumps({: ,
: fetch_errors})); SystemExit
roles = {( r.upper() == r.upper()) r ROLES}
():
a = chk.get() {}
():
lst = value (value, ) [value]
target lst
a a[] in_list(a[], OS_TYPE): ,
a a[] in_list(a[], DB_TYPE): ,
a a[] in_list(a[], STORAGE_TYPE): ,
a a[]:
rolelst = {x.upper() x (a[] (a[], ) [a[]])}
(rolelst & roles):
,
a a[] :
v = a[]
HA_TYPE == :
v ((v, ) v):
,
:
v : ,
(v, ) HA_TYPE v: ,
a a[] :
HA_AGENT == :
,
in_list(a[], HA_AGENT):
,
,
applicable, seen, filtered = [], (), {}
chk all_checks:
cid = chk.get(, )
cid seen:
ok, why = applies(chk)
ok:
seen.add(cid); applicable.append(chk)
why:
filtered[why] = filtered.get(why, ) +
evaluatable = [c c applicable c.get() == c.get()]
data_only = [c c applicable c evaluatable]
configs = {}
CONFIG_DIR.is_dir():
p CONFIG_DIR.rglob():
p.is_file():
:
configs[(p.relative_to(CONFIG_DIR)).replace(, )] = p.read_text(errors=)
Exception:
configs:
(json.dumps({: }))
SystemExit
():
check.get() == :
,
cmd = (check.get() {}).get(, )
cmd:
,
m = re.search(, cmd)
m:
param = m.group()
data = configs.get(, )
data:
,
line data.split():
line:
k, _, v = line.partition()
k.strip() == param:
v.strip(),
,
m = re.search(, cmd)
m:
mount = m.group()
data = configs.get(, )
data:
,
line data.strip().split():
parts = line.split()
(parts) >= parts[-] == mount:
parts[],
,
cmd:
data = configs.get(, )
data:
,
m2 = re.search(, data)
(m2.group() m2 data.strip()),
cmd:
data = configs.get(, )
(data.strip(), ) data (, )
cmd cmd:
data = configs.get(, )
data:
,
(( l data.split() l.lower())),
cmd cmd:
data = configs.get(, )
data:
,
line data.split():
line.strip().startswith():
parts = line.split()
(parts) >= :
parts[],
,
cmd:
data = configs.get(, )
data:
,
line data.split():
line line:
line.split(, )[].strip(),
,
cmd cmd:
data = configs.get(, )
data :
,
(data.strip() data.strip() ),
cmd cmd:
data = configs.get(, )
(data.strip(), ) data (, )
,
():
vtype = check.get()
vargs = check.get() {}
actual :
,
actual_s = .join((actual).strip().split())
vtype == :
expected = .join((vargs.get(, )).strip().split())
actual_s.lower() == expected.lower():
,
,
vtype == :
:
n = (actual_s)
ValueError:
,
lo, hi = vargs.get(), vargs.get()
lo n < (lo):
,
hi n > (hi):
,
,
vtype == :
valid = vargs.get(, []) []
actual_s.lower() [(v).strip().lower() v valid]:
,
,
,
results, failures = [], []
chk evaluatable:
actual, skip = extract_actual(chk)
actual :
status, detail = , skip
:
status, detail = compare(chk, actual)
item = {
: chk.get(),
: chk.get(),
: chk.get(),
: chk.get(),
: status,
: detail,
: chk.get(),
}
results.append(item)
status == :
failures.append(item)
summary = {
: ( r results r[] == ),
: (failures),
: ( r results r[] == ),
: (data_only),
}
(json.dumps({
: SID, : HOST,
: {
: {: , : (all_checks),
: fetch_errors },
: {: , : (configs),
: (CONFIG_DIR)},
},
: {
: (all_checks),
: filtered,
: (applicable),
: (evaluatable),
},
: summary,
: failures,
: results,
}, indent=))
Step 3 — Format and present results
Use the printed JSON to produce a compliance report. Do not invent any check IDs, expected values, or references — quote them verbatim from the JSON.
Suggested format:
<SID> — STAF Config Compliance Report
Data Sources:
STAF checks: GitHub live (<staf.total> total) — Azure/sap-automation-qa @ main
Config data: blob <configs.dir> (<configs.file_count> files)
Check Coverage:
Total STAF checks: <staf.total>
Filtered out: <sum of filtered_out> (os_type: N, role: N, ...)
Applicable: <staf.applicable>
├─ Evaluated: <pass + fail> (<pass> pass, <fail> fail)
├─ Not evaluated: <not_evaluated> (commands this skill does not extract — see below)
└─ Data-only: <data_only> (info/section/table — no expected value)
══════════════════════════════════════
FAILURES (list every entry from .failures verbatim):
❌ <id> <name> — <detail> [<severity>]
Refs: <references>
...
══════════════════════════════════════
SUMMARY: <pass>/<evaluated> PASS, <fail> FAIL
Presentation rules:
- Show
data_sources first so the user knows where the data came from.
- List every failure from
report.failures — never truncate or summarize.
- If
summary.not_evaluated is high, explain that this skill only extracts values for nine common command patterns (sysctl, df -T, THP, tuned-adm, fstrim, swap, DefaultTasksMax, softdog, uname). Cluster / ANF / Azure-collector / arbitrary-command checks return not_evaluated with a reason — that is expected, not a bug.
- If
configs.file_count == 0, do not report any PASS/FAIL — say configs are missing and stop.
- If
staf.fetch_errors is non-empty, surface it so the user knows partial STAF data was used.
References