File cleanoldsepoldir.sh of Package selinux-policy

#!/usr/bin/env bash
# Copyright (C) 2025 SUSE Linux
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

# This utility safely clean up the legacy /var/lib/selinux directory after migration.

# Helper function to find active SELinux modules.
_get_modules() {
    local search_dir="$1"
    local -n return_array=$2
    local temp_modules=()

    for policy in minimum sandbox targeted; do
        local modules_path="${search_dir}/${policy}/active/modules"
        if [[ -d "$modules_path" ]]; then
            mapfile -t current_modules < <(find "$modules_path" -maxdepth 2 -type d '!' -empty | grep -vE '/(modules/|100|200|400|disabled)$')
            temp_modules+=("${current_modules[@]}")
        fi
    done
    IFS=$'\n' read -r -d '' -a return_array < <(printf "%s\n" "${temp_modules[@]}" | sort -u && printf '\0')
}

# Checks for custom SELinux modules that may not have been migrated.
check_custom_selinux_modules () {
    echo "INFO: Checking for possibly not migrated custom selinux modules in /var/lib/selinux/..."
    local old_selinux_dir="/var/lib/selinux"
    local new_selinux_dir="${target_dir}"
    local custom_package_dir="/usr/share/selinux/packages"
    local old_modules_unique
    _get_modules "$old_selinux_dir" old_modules_unique
    local new_modules_unique
    _get_modules "$new_selinux_dir" new_modules_unique
    local custom_modules=()
    if [ -d "${custom_package_dir}" ]; then
        mapfile -t current_modules < <(find "${custom_package_dir}" -maxdepth 2 -type f '!' -empty)
        custom_modules+=("${current_modules[@]}")
    fi
    IFS=$'\n' read -r -d '' -a custom_modules_unique < <(printf "%s\n" "${custom_modules[@]}" | sort -u && printf '\0')
    # Compare the old and new module lists to identify any missing modules.
    local missing_modules=()
    local modules_with_packages=()
    local modules_without_packages=()
    local -A new_modules_lookup
    for new_module_path in "${new_modules_unique[@]}"; do
        new_modules_lookup["$(basename "$new_module_path")"]=1
    done
    for old_module_path in "${old_modules_unique[@]}"; do
        local old_module_basename
        old_module_basename=$(basename "$old_module_path")
        if [[ -z "${new_modules_lookup[$old_module_basename]}" ]]; then
            missing_modules+=("$old_module_basename (module dir: $old_module_path)")
        fi
    done
    # For each missing module, check if it belongs to an installed RPM package.
    if [ ${#missing_modules[@]} -eq 0 ]; then
        echo "INFO: No custom modules missing from migration."
    else
        echo "INFO: Found possible missing custom selinux modules:"
        for module_info in "${missing_modules[@]}"; do
            local package_found="false"
            local module_basename="${module_info%% *}"
            for custom_module_path in "${custom_modules_unique[@]}"; do
                if [[ "$custom_module_path" =~ ${module_basename}.pp.(bz2|gz)$ ]]; then
                    local rpm_owner
                    rpm_owner=$(rpm -qf --queryformat "%{NAME}" "${custom_module_path}" 2>/dev/null)
                    if [[ -n "$rpm_owner" ]]; then
                        modules_with_packages+=("$module_info (from package: $rpm_owner)")
                        package_found="true"
                        break
                    fi
                fi
            done
            if [[ "$package_found" == "false" ]]; then
                modules_without_packages+=("$module_info")
            fi
        done
        # Print a report categorizing the missing modules for the user.
        echo "---"
        echo "These modules have corresponding packages; try reinstalling them:"
        if [ ${#modules_with_packages[@]} -eq 0 ]; then
            echo " (None)"
        else
            printf " * %s\n" "${modules_with_packages[@]}"
        fi
        echo
        echo "These modules do not have corresponding packages available:"
        if [ ${#modules_without_packages[@]} -eq 0 ]; then
            echo " (None)"
        else
            printf " * %s\n" "${modules_without_packages[@]}"
        fi
	echo ""
	echo "- modules in 100 dir by default comes with system policy package update -"
        echo "---"
    fi
    echo
}

# Deletes the old /var/lib/selinux directory and updates marker files.
delete_var_lib_selinux () {
    if [ ! -d "/var/lib/selinux" ]; then
        echo "INFO: Directory \`/var/lib/selinux\` does not exist. Nothing to do."
        return
    fi
    echo "INFO: Deleting /var/lib/selinux..."
    if find /var/lib/selinux -depth -print -delete; then
        echo "INFO: Successfully deleted /var/lib/selinux."
        if [[ -f "${target_dir}/${target_file}" ]]; then
            rm -f "${target_dir}/${target_file}" && \
            echo "DO NOT DELETE THIS FILE - Part of SELinux policy root path migration on transactional or snapshoted systems." > "${target_dir}/${target_file2}"
        fi
    else
        echo "ERROR: Failed to delete /var/lib/selinux." >&2
        exit 1
    fi
}

# Checks a specific location (snapshot or overlay) for the migration marker file.
check_path_for_file() {
    local type_str="$1"
    local number="$2"
    local base_path="$3"
    local suffix="$4"

    # Construct the full path to the file to check
    local full_path="${base_path}/${number}${suffix}${target_dir}/${target_file}"

    echo -n "  Checking ${type_str} ${number}... "
    if [ -f "${full_path}" ]; then
        echo "Found."
        return 0
    else
        echo "Not found."
        return 1
    fi
}


## Main function

# Definitions
target_dir="/etc/selinux"
target_file="tmpselipoldir_migrated"
target_file2="tmpselipoldir_deleted"

# Handle specific flags first
if [[ "$1" == "--check-custom-selinux-modules" ]]; then
    check_custom_selinux_modules
    exit 0
elif [[ "$1" == "-h" || "$1" == "--help" ]]; then
    echo "This script checks if it is safe to remove the old /var/lib/selinux directory."
    echo
    echo "Usage:"
    echo "  $0            (Checks snapshots and deletes /var/lib/selinux if safe)"
    echo "  $0 --check-custom-selinux-modules (Checks for unmigrated custom modules)"
    echo "  $0 -h|--help (Displays this help message)"
    exit 0
elif [[ -n "$1" ]]; then
    echo "Wrong parameter: $1. Use -h for help."
    exit 1
fi

# --- Default Execution: Check system and decide whether to delete ---

# Run essential pre-checks that can end the script early
if [[ ! -x "$(command -v snapper)" ]]; then
    echo "ERROR: snapper command not found. This script requires snapper." >&2
    exit 1
fi
if [ -f "${target_dir}/${target_file2}" ]; then
    echo "INFO: Cleanup already completed (${target_file2} exists). Nothing to do."
    exit 0
elif [ ! -f "${target_dir}/${target_file}" ]; then
    echo "INFO: Migration has not occurred yet (${target_file} is missing). Nothing to do."
    exit 0
fi

# Perform Btrfs snapshot check
echo "INFO: Checking all Btrfs snapshots for migration marker file..."
btrfs_check_passed=false
snapshots_without_file=()
snapshot_list=$(snapper --no-headers --machine-readable csv list | awk -F, '$3 >= 1 {print $3}')

if [[ -z "$snapshot_list" ]]; then
    echo "INFO: No snapshots found. Btrfs check passed."
    btrfs_check_passed=true
else
    mapfile -t snapshots <<< "$snapshot_list"
    for snapshot_num in "${snapshots[@]}"; do
        if ! check_path_for_file "snapshot" "$snapshot_num" "/.snapshots" "/snapshot"; then
            snapshots_without_file+=("$snapshot_num")
        fi
    done

    if [[ ${#snapshots_without_file[@]} -eq 0 ]]; then
        echo "INFO: All snapshots contain the migration file. Btrfs check passed."
        btrfs_check_passed=true
    fi
fi

# Perform conditional legacy OverlayFS check
overlayfs_check_passed=true
lowest_layer_failed=""
tu_path=$(command -v transactional-update)
if [[ -n "$tu_path" ]]; then
    tu_version=$(transactional-update --version | awk '/transactional-update/ {print $NF}')
    if [[ "${tu_version%%.*}" -lt "5" ]]; then
        echo "INFO: Detected legacy transactional-update < 5. Performing additional OverlayFS check..."
        overlay_dir="/var/lib/overlay"
        if [ -d "$overlay_dir" ] && compgen -G "${overlay_dir}/*/etc" > /dev/null; then
            echo "INFO: /etc is managed by OverlayFS."
            mapfile -t layers < <(find "$overlay_dir" -maxdepth 1 -type d -name '[0-9]*' -printf '%f\n' | sort -n)
            if [[ ${#layers[@]} -gt 0 ]]; then
                lowest_layer="${layers[0]}"
		if ! check_path_for_file "lowest overlay layer" "$lowest_layer" "/var/lib/overlay" ""; then
                    overlayfs_check_passed=false
                    lowest_layer_failed=$lowest_layer
                fi
            fi
        else
             echo "INFO: /etc is not managed by OverlayFS. Skipping check."
        fi
    fi
fi

# Make the final decision based on the results of all checks
echo "---"
if [[ "$btrfs_check_passed" == "true" && "$overlayfs_check_passed" == "true" ]]; then
    echo "INFO: All checks passed. It is safe to proceed with deletion."
    delete_var_lib_selinux
else
    echo "WARNING: Cannot delete /var/lib/selinux. One or more checks failed:"
    if [[ "$btrfs_check_passed" == "false" ]]; then
        echo " - Not all Btrfs snapshot(s) are migrated."
    fi
    if [[ "$overlayfs_check_passed" == "false" ]]; then
        echo " - The lowest OverlayFS layer (${lowest_layer_failed}) is not migrated."
    fi
    exit 0
fi

exit 0
openSUSE Build Service is sponsored by