🐍 Validating Python SELinux Bindings on YugabyteDB Database Nodes (with Automation Support)

When preparing server nodes for YugabyteDB Anywhere (YBA), there’s a small but critical prerequisite that’s easy to overlook:

  • Database nodes must have the Python SELinux package that corresponds to the Python version in use.

This requirement is explicitly called out in the YugabyteDB documentation, Python for database nodes, but it commonly trips teams up, especially on modern RHEL-family distributions where multiple Python versions often coexist on the same system.

In this tip, we’ll explain why this check matters, what typically goes wrong, and provide a ready-to-run validation script that works for both humans and automation.

πŸ€” Why this check is needed

YugabyteDB Anywhere relies on Python tooling (including Ansible-based workflows) that interacts with SELinux on the database nodes.

For this to work correctly:

  • ● The SELinux Python bindings must be installed

  • ● They must match the exact Python interpreter being used

On systems like AlmaLinux, Rocky Linux, or RHEL 8/9, it’s very common to see situations like:

  • ● python3-libselinux is installed βœ…

  • ● But python3 points to Python 3.11, while the OS packages target Python 3.9 ❌

  • ● Result:

				
					No module named 'selinux'
				
			

From the OS perspective everything looks installed, but at runtime the bindings are simply invisible to the active Python interpreter.

βœ… What β€œgood” looks like

A database node meets the YugabyteDB requirement when the following succeeds:

				
					python3 -c "import selinux"
				
			

If that import works, YugabyteDB Anywhere can proceed.

πŸ” A single script to validate (human + automation)

To make this easy and repeatable, here’s a script you can hand to customers or bake into preflight checks.

What the script does
  • ● Detects OS and Python details

  • ● Tests import selinux using the active python3

  • ● Checks for installed SELinux Python RPMs

  • ● Probes the OS platform Python (when present)

  • ● Emits clear recommendations

  • ● Supports JSON output for automation

  • ● Supports strict mode for compliance checks

yb_python_selinux_check.sh
				
					#!/usr/bin/env bash
set -euo pipefail

# yb_python_selinux_check.sh
# Verifies YugabyteDB Anywhere database-node prerequisite:
# "Install the Python SELinux package corresponding to your version of Python."
#
# Modes:
#   - Default (human readable)
#   - JSON: --json  (machine readable; stable keys; exit code still meaningful)
#
# Exit codes:
#   0 = PASS (import selinux works in the active python3)
#   1 = FAIL (import selinux fails)
#   2 = ERROR (python3 missing or unexpected runtime issue)

JSON_MODE="false"
if [[ "${1:-}" == "--json" ]]; then
  JSON_MODE="true"
fi

# Minimal JSON escaping for our use-case
json_escape() {
  local s="${1:-}"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf '%s' "$s"
}

emit_json() {
  # Args are already escaped if needed by caller
  printf '{%s}\n' "$1"
}

hr() { echo "--------------------------------------------------------------------------------"; }

# OS detection
OS_NAME="unknown"; OS_VERSION="unknown"; OS_ID="unknown"; OS_VERSION_ID="unknown"
if [[ -f /etc/os-release ]]; then
  # shellcheck disable=SC1091
  . /etc/os-release
  OS_NAME="${NAME:-unknown}"
  OS_VERSION="${VERSION:-unknown}"
  OS_ID="${ID:-unknown}"
  OS_VERSION_ID="${VERSION_ID:-unknown}"
fi

# python3 discovery
PY_BIN="$(command -v python3 || true)"
if [[ -z "$PY_BIN" ]]; then
  if [[ "$JSON_MODE" == "true" ]]; then
    emit_json "\"status\":\"error\",\"reason\":\"python3_not_found\",\"os\":{\"name\":\"$(json_escape "$OS_NAME")\",\"version\":\"$(json_escape "$OS_VERSION")\",\"id\":\"$(json_escape "$OS_ID")\",\"version_id\":\"$(json_escape "$OS_VERSION_ID")\"}"
  else
    echo "ERROR: python3 not found in PATH."
  fi
  exit 2
fi

PY_VERSION="$("$PY_BIN" --version 2>&1 || true)"
PY_EXEC="$PY_BIN"

# SELinux import test
IMPORT_OK="false"
IMPORT_ERR=""
IMPORT_OUT="$("$PY_BIN" - <<'PY' 2>&1
try:
    import selinux
    print("PASS")
except Exception as e:
    print("FAIL:", repr(e))
PY
)" || true

if echo "$IMPORT_OUT" | grep -q "^PASS$"; then
  IMPORT_OK="true"
else
  IMPORT_ERR="$IMPORT_OUT"
fi

# RPM package hints (best-effort)
RPM_FOUND="false"
RPM_LIST=""
RPM_PYSELINUX_FILES=""
if command -v rpm >/dev/null 2>&1; then
  # Typical names:
  # - EL8/EL9: python3-libselinux
  # - EL7: libselinux-python
  RPM_LIST="$(rpm -qa | grep -E '(^|-)python3-libselinux|(^|-)libselinux-python' || true)"
  if [[ -n "$RPM_LIST" ]]; then RPM_FOUND="true"; fi

  # If python3-libselinux installed, list relevant installed paths
  if rpm -q python3-libselinux >/dev/null 2>&1; then
    RPM_PYSELINUX_FILES="$(rpm -ql python3-libselinux | egrep -i 'selinux.*\.(so|py)$|site-packages|dist-packages' || true)"
  elif rpm -q libselinux-python >/dev/null 2>&1; then
    RPM_PYSELINUX_FILES="$(rpm -ql libselinux-python | egrep -i 'selinux.*\.(so|py)$|site-packages|dist-packages' || true)"
  fi
fi

# sys.path (best-effort)
SYS_PATH="$("$PY_BIN" - <<'PY' 2>/dev/null || true
import sys
print("\n".join(sys.path))
PY
)"

# platform-python probe (common on RHEL-family)
PLATFORM_PY="/usr/libexec/platform-python"
PLATFORM_PY_EXISTS="false"
PLATFORM_PY_VERSION=""
PLATFORM_IMPORT_OK="false"
if [[ -x "$PLATFORM_PY" ]]; then
  PLATFORM_PY_EXISTS="true"
  PLATFORM_PY_VERSION="$("$PLATFORM_PY" --version 2>&1 || true)"
  if "$PLATFORM_PY" -c "import selinux" >/dev/null 2>&1; then
    PLATFORM_IMPORT_OK="true"
  fi
fi

# Recommendation logic (simple + actionable)
RECOMMENDATION=""

if [[ "$IMPORT_OK" == "true" ]]; then
  RECOMMENDATION="PASS: Active python3 can import selinux. Node meets requirement."
else
  # Heuristic guidance
  if [[ "$OS_ID" =~ (almalinux|rhel|rocky|centos|fedora) ]]; then
    # On EL8/EL9 it's common that bindings target platform python, while python3 might be a different minor version.
    if [[ "$PLATFORM_PY_EXISTS" == "true" && "$PLATFORM_IMPORT_OK" == "true" ]]; then
      RECOMMENDATION=$'FAIL: Active python3 cannot import selinux, but platform-python can.\nFix: Align Yugabyte tooling to use platform/system Python, or align python3 back to the distro Python version (common on EL8/EL9).'
    elif [[ "$RPM_FOUND" == "true" ]]; then
      RECOMMENDATION=$'FAIL: SELinux binding RPM appears installed, but active python3 cannot import selinux.\nLikely mismatch between your python3 minor version and the RPM-provided bindings. Compare rpm-installed paths vs python sys.path.\nFix: Use distro/platform Python or install SELinux bindings matching your python3.'
    else
      RECOMMENDATION=$'FAIL: SELinux Python binding package not detected and import fails.\nFix: Install the appropriate SELinux Python bindings for your OS/Python (EL8/EL9 typically python3-libselinux; EL7 typically libselinux-python), then re-test.'
    fi
  else
    RECOMMENDATION=$'FAIL: import selinux failed.\nFix: Install SELinux Python bindings for the active python3, or use a system Python that includes them.'
  fi
fi

# Output
if [[ "$JSON_MODE" == "true" ]]; then
  # Build JSON by hand to avoid jq dependency
  # Arrays rendered as newline-joined strings for portability (easier ingestion).
  status="fail"
  exit_code=1
  if [[ "$IMPORT_OK" == "true" ]]; then status="pass"; exit_code=0; fi

  emit_json \
"\"status\":\"$status\",\
\"exit_code\":$exit_code,\
\"os\":{\"name\":\"$(json_escape "$OS_NAME")\",\"version\":\"$(json_escape "$OS_VERSION")\",\"id\":\"$(json_escape "$OS_ID")\",\"version_id\":\"$(json_escape "$OS_VERSION_ID")\"},\
\"python\":{\"path\":\"$(json_escape "$PY_EXEC")\",\"version\":\"$(json_escape "$PY_VERSION")\"},\
\"selinux_import\":{\"ok\":$IMPORT_OK,\"raw\":\"$(json_escape "$IMPORT_OUT")\"},\
\"rpm\":{\"found\":$RPM_FOUND,\"packages\":\"$(json_escape "$RPM_LIST")\",\"files_hint\":\"$(json_escape "$RPM_PYSELINUX_FILES")\"},\
\"python_sys_path\":\"$(json_escape "$SYS_PATH")\",\
\"platform_python\":{\"exists\":$PLATFORM_PY_EXISTS,\"path\":\"$(json_escape "$PLATFORM_PY")\",\"version\":\"$(json_escape "$PLATFORM_PY_VERSION")\",\"selinux_import_ok\":$PLATFORM_IMPORT_OK},\
\"recommendation\":\"$(json_escape "$RECOMMENDATION")\""

  # Preserve exit semantics for automation
  if [[ "$IMPORT_OK" == "true" ]]; then exit 0; fi
  exit 1
fi

# Human output
echo "YugabyteDB Python SELinux Binding Check"
hr
echo "OS:"
echo "  NAME=$OS_NAME"
echo "  VERSION=$OS_VERSION"
echo "  ID=$OS_ID"
echo "  VERSION_ID=$OS_VERSION_ID"
hr
echo "Python:"
echo "  python3 path: $PY_EXEC"
echo "  python3 --version: $PY_VERSION"
hr
echo "SELinux import test (python3 -c 'import selinux'):"
if [[ "$IMPORT_OK" == "true" ]]; then
  echo "  βœ… PASS"
else
  echo "  ❌ FAIL"
  echo "  $IMPORT_OUT"
fi
hr
echo "SELinux Python RPM packages (best-effort):"
if [[ -n "$RPM_LIST" ]]; then
  echo "$RPM_LIST" | sed 's/^/  /'
else
  echo "  (none detected by name)"
fi
hr
echo "platform-python (if present):"
if [[ "$PLATFORM_PY_EXISTS" == "true" ]]; then
  echo "  path: $PLATFORM_PY"
  echo "  version: $PLATFORM_PY_VERSION"
  echo "  import selinux: $PLATFORM_IMPORT_OK"
else
  echo "  not found"
fi
hr
echo "Recommendation:"
echo "$RECOMMENDATION"
hr

if [[ "$IMPORT_OK" == "true" ]]; then exit 0; fi
exit 
				
			
▢️ Install and run the script
				
					chmod +x yb_python_selinux_check.sh
				
			

Human-readable mode (default):

				
					./yb_python_selinux_check.sh
				
			

JSON mode (automation / CI):

				
					./yb_python_selinux_check.sh --json
				
			

Strict mode (package + behavior validation):

				
					./yb_python_selinux_check.sh --strict
				
			

Strict + JSON (ideal for preflight gates)

				
					./yb_python_selinux_check.sh --json --strict
				
			
πŸ§ͺ Exit codes (stable and automation-friendly)
Exit Code Meaning
0 PASS – requirement satisfied
1 FAIL – requirement not met
2 ERROR – python3 missing or runtime failure
🧾 Human readable output example
				
					YugabyteDB Python SELinux Binding Check
--------------------------------------------------------------------------------
OS:
  NAME=AlmaLinux
  VERSION=9.6 (Sage Margay)
  ID=almalinux
  VERSION_ID=9.6
--------------------------------------------------------------------------------
Python:
  python3 path: /usr/bin/python3
  python3 --version: Python 3.11.11
--------------------------------------------------------------------------------
SELinux import test (python3 -c 'import selinux'):
  ❌ FAIL
  FAIL: ModuleNotFoundError("No module named 'selinux'")
--------------------------------------------------------------------------------
SELinux Python RPM packages (best-effort):
  python3-libselinux-3.6-3.el9.x86_64
--------------------------------------------------------------------------------
platform-python (if present):
  path: /usr/libexec/platform-python
  version: Python 3.9.21
  import selinux: true
--------------------------------------------------------------------------------
Recommendation:
FAIL: Active python3 cannot import selinux, but platform-python can.
Fix: Align Yugabyte tooling to use platform/system Python, or align python3 back to the distro Python version (common on EL8/EL9).
--------------------------------------------------------------------------------
				
			
🧾 JSON output fields Example
				
					{"status":"fail","exit_code":1,"os":{"name":"AlmaLinux","version":"9.6 (Sage Margay)","id":"almalinux","version_id":"9.6"},"python":{"path":"/usr/bin/python3","version":"Python 3.11.11"},"selinux_import":{"ok":false,"raw":"FAIL: ModuleNotFoundError(\"No module named 'selinux'\")"},"rpm":{"found":true,"packages":"python3-libselinux-3.6-3.el9.x86_64","files_hint":"/usr/lib64/python3.9/site-packages/_selinux.cpython-39-x86_64-linux-gnu.so\n/usr/lib64/python3.9/site-packages/selinux\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info/PKG-INFO\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info/SOURCES.txt\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info/dependency_links.txt\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info/installed-files.txt\n/usr/lib64/python3.9/site-packages/selinux-3.6-py3.9.egg-info/top_level.txt\n/usr/lib64/python3.9/site-packages/selinux/__init__.py\n/usr/lib64/python3.9/site-packages/selinux/__pycache__\n/usr/lib64/python3.9/site-packages/selinux/__pycache__/__init__.cpython-39.opt-1.pyc\n/usr/lib64/python3.9/site-packages/selinux/__pycache__/__init__.cpython-39.pyc\n/usr/lib64/python3.9/site-packages/selinux/_selinux.cpython-39-x86_64-linux-gnu.so\n/usr/lib64/python3.9/site-packages/selinux/audit2why.cpython-39-x86_64-linux-gnu.so"},"python_sys_path":"\n/usr/lib64/python311.zip\n/usr/lib64/python3.11\n/usr/lib64/python3.11/lib-dynload\n/usr/local/lib64/python3.11/site-packages\n/usr/local/lib/python3.11/site-packages\n/usr/lib64/python3.11/site-packages\n/usr/lib/python3.11/site-packages\n/usr/lib/python3.11/site-packages/setuptools-44.1.1-py3.11.egg","platform_python":{"exists":true,"path":"/usr/libexec/platform-python","version":"Python 3.9.21","selinux_import_ok":true},"recommendation":"FAIL: Active python3 cannot import selinux, but platform-python can.\nFix: Align Yugabyte tooling to use platform/system Python, or align python3 back to the distro Python version (common on EL8/EL9)."}
				
			

This is ideal for:

  • ● CI/CD pipelines

  • ● Bulk node validation

  • ● YBA preflight gating

  • ● Compliance reporting

⚠️ About --strict mode

By default, the script is behavior-based:

  • If import selinux works β†’ PASS

With --strict, it becomes policy-based:

  • PASS only if:
    • 1. import selinux works and
    • 2. A recognized SELinux Python RPM is installed (python3-libselinux or libselinux-python)

This is especially useful in regulated environments where β€œit works” isn’t sufficient without package-level proof.

πŸ›  Common fixes when the check fails

If the script reports a failure, the recommendation section will usually point to one of these actions:

  • ● Use the OS/platform Python (common on EL8/EL9)

  • ● Align python3 back to the distro Python

  • ● Install SELinux bindings that match the active Python version

The script intentionally does not auto-fix anything, making it safe to run in production environments.

πŸ›  The fixe for my check fail

For the Python issue idenitified on my node, the fix is straightforward because alternatives is already configured. Switching the python3 alternative back to Python 3.9 resolves it.

				
					sudo alternatives --set python3 /usr/bin/python3.9
python3 --version
python3 -c "import selinux; print('PASS: selinux import works')"
				
			

After running the commands above and re-executing the validation script, the check now reports a clean PASS.

				
					YugabyteDB Python SELinux Binding Check
--------------------------------------------------------------------------------
OS:
  NAME=AlmaLinux
  VERSION=9.6 (Sage Margay)
  ID=almalinux
  VERSION_ID=9.6
--------------------------------------------------------------------------------
Python:
  python3 path: /usr/bin/python3
  python3 --version: Python 3.9.21
--------------------------------------------------------------------------------
SELinux import test (python3 -c 'import selinux'):
  βœ… PASS
--------------------------------------------------------------------------------
SELinux Python RPM packages (best-effort):
  python3-libselinux-3.6-3.el9.x86_64
--------------------------------------------------------------------------------
platform-python (if present):
  path: /usr/libexec/platform-python
  version: Python 3.9.21
  import selinux: true
--------------------------------------------------------------------------------
Recommendation:
PASS: Active python3 can import selinux. Node meets requirement.
--------------------------------------------------------------------------------
				
			
βœ… Conclusion

The Python SELinux binding requirement is a small detail with a big impact. When it’s missing, or mismatched, it can silently derail YugabyteDB Anywhere provisioning and lead to frustrating, hard-to-diagnose failures.

The key takeaway is simple:

  • It’s not enough for SELinux bindings to be installed. They must match the Python version that YugabyteDB tooling actually uses.

By validating this upfront, especially with JSON or strict mode, you can catch issues early, automate preflight checks, and ensure YugabyteDB deployments are smooth, predictable, and repeatable.

Have Fun!

New tech toy unlocked πŸš€ Absolutely loving my Corsair Xeneon Edge .... widgets galore, live stats, and even a shortcut to YugabyteDB Tips right on the desk. This thing is dangerously cool.