Freeze HA scoring and document reported production failover

This commit is contained in:
Lyncoln
2026-09-12 18:07:28 -04:00
parent c8636ed6d4
commit 488a1672d7
29 changed files with 2285 additions and 46 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ tar --sort=name --mtime="@$SOURCE_DATE_EPOCH" --owner=0 --group=0 --numeric-owne
--exclude='__pycache__' --exclude='*.pyc' \
--transform="s,^,ocf-managed-container-$package_version/," \
-czf "$build_dir/SOURCES/ocf-managed-container-$package_version.tar.gz" \
README.md HANDOFF.md refactored reference tests packaging scripts
README.md HANDOFF.md docs refactored reference tests packaging scripts notes/PROMOTION-SCORING.md notes/DISCONNECTED-STANDBY.md
# Freeze identity into the SRPM spec so rpmbuild --rebuild needs no Git metadata
# or external macro definitions and retains the same version and release.
sed -e "s/^Version:.*/Version: $package_version/" \
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# HISTORICAL LAB TOOL: not a production/scoring installer; see docs/operations.md.
# One-time installation of the modular baseline on the confirmed lab standby.
# Run with sudo in your terminal. No service or PostgreSQL lifecycle actions.
set -euo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
project_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
provider_dir=/usr/lib/ocf/resource.d/loxthelion
agent="$provider_dir/managed-container"
expected_hash=5eae1f3dba0586dc06105e4dd7a9b8809a5c9961452abe7654dcfce77c3c5ce2
fail() { printf 'STOP: %s\n' "$*" >&2; exit 1; }
[[ "$EUID" == 0 ]] || fail 'Run this installer with sudo in your terminal.'
[[ "$(hostname -s)" == DAVID-ELITEDESK ]] || fail 'This installer is only for DAVID-ELITEDESK.'
[[ "$(crm_attribute --type crm_config --name cluster-name --query --quiet)" == postgres-ra-test ]] || fail 'Unexpected cluster name.'
[[ "$(crm_attribute --type crm_config --name maintenance-mode --query --quiet)" == true ]] || fail 'Pacemaker maintenance-mode must already be true.'
[[ "$(sha256sum "$agent" | cut -d ' ' -f1)" == "$expected_hash" ]] || fail 'Installed agent differs from the confirmed reference.'
[[ ! -e "$provider_dir/managed-container.d" ]] || fail 'A module directory already exists; review it before installing.'
cd "$project_dir"
[[ "$(sha256sum refactored/managed-container | cut -d ' ' -f1)" == "c91f09adfdeb45a3be1a72df3f5ca5b02fd4555dec4397ad944eab439ace0b89" ]] || fail 'This installer is for the original modular baseline only; the scoring enhancement requires separate review.'
sha256sum -c notes/REFACTOR-SHA256SUMS
for file in refactored/managed-container refactored/managed-container.d/*.sh; do
bash -n "$file"
done
# Match the recorded pg-ha resource parameters and avoid inherited test overrides.
run_ra() {
env -i PATH="$PATH" OCF_ROOT=/usr/lib/ocf OCF_RESOURCE_INSTANCE=pg-ha \
OCF_RESKEY_service_user=david OCF_RESKEY_service_name=postgres.service \
OCF_RESKEY_container_name=postgres OCF_RESKEY_pg_os_user=postgres \
OCF_RESKEY_db_user=david OCF_RESKEY_db_name=david \
"$1" "$2"
}
run_ra "$agent" validate-all
run_ra "$agent" monitor || fail 'The original agent must report a healthy unpromoted standby (rc=0).'
mkdir -p /var/backups
backup_dir="$(mktemp -d /var/backups/managed-container-ELITEDESK.XXXXXXXX)"
cp -a "$agent" "$backup_dir/managed-container"
printf 'Original agent backed up to: %s\n' "$backup_dir"
stage_dir="$(mktemp -d "$provider_dir/.managed-container-install.XXXXXXXX")"
install -d -o root -g root -m 0755 "$stage_dir/managed-container.d"
install -o root -g root -m 0755 refactored/managed-container "$stage_dir/managed-container"
install -o root -g root -m 0644 refactored/managed-container.d/*.sh "$stage_dir/managed-container.d/"
run_ra "$agent" meta-data > "$backup_dir/original-metadata.xml"
run_ra "$stage_dir/managed-container" meta-data > "$backup_dir/staged-metadata.xml"
cmp "$backup_dir/original-metadata.xml" "$backup_dir/staged-metadata.xml"
run_ra "$stage_dir/managed-container" validate-all
run_ra "$stage_dir/managed-container" monitor || fail 'Staged agent must report a healthy standby.'
published=false
rollback_on_error() {
local rc=$?
trap - EXIT
if [[ "$rc" != 0 && "$published" == true ]]; then
printf 'Installation/check failed; restoring the original entrypoint.\n' >&2
if install -o root -g root -m 0755 "$backup_dir/managed-container" "$stage_dir/rollback-agent" &&
mv -f "$stage_dir/rollback-agent" "$agent" && restorecon "$agent"; then
printf 'Original restored. New modules remain unused beside it for inspection.\n' >&2
else
printf 'Automatic restoration FAILED. Original backup: %s/managed-container\n' "$backup_dir" >&2
fi
fi
exit "$rc"
}
trap rollback_on_error EXIT
# Recheck immediately before changing the installed files.
[[ "$(crm_attribute --type crm_config --name maintenance-mode --query --quiet)" == true ]] || fail 'Maintenance mode changed; refusing installation.'
published=true
mv "$stage_dir/managed-container.d" "$provider_dir/managed-container.d"
mv -f "$stage_dir/managed-container" "$agent"
restorecon -RF "$agent" "$provider_dir/managed-container.d"
run_ra "$agent" meta-data > "$backup_dir/installed-metadata.xml"
cmp "$backup_dir/original-metadata.xml" "$backup_dir/installed-metadata.xml"
run_ra "$agent" validate-all
run_ra "$agent" monitor || fail 'Installed agent did not report a healthy standby.'
[[ "$(crm_attribute --type crm_config --name maintenance-mode --query --quiet)" == true ]] || fail 'Maintenance mode is no longer true.'
for file in refactored/managed-container refactored/managed-container.d/*.sh; do
cmp "$file" "$provider_dir/${file#refactored/}"
done
sha256sum "$agent" "$provider_dir/managed-container.d/"*.sh > "$backup_dir/installed-SHA256SUMS"
cat "$backup_dir/installed-SHA256SUMS"
published=false
trap - EXIT
rmdir "$stage_dir"
printf '\nPASS: refactored RA installed; metadata identical, validate-all=0, monitor=0.\n'
printf 'Maintenance mode remains true. No service restart or HA transition was requested.\n'
printf 'Backup and validation evidence: %s\n' "$backup_dir"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Print (never execute) exact cleanup commands from a supplied CIB snapshot."""
import argparse
from pathlib import Path
import shlex
import sys
import xml.etree.ElementTree as ET
def plan(path, clone_id, preferred_node):
root = ET.parse(path).getroot()
clones = [c for c in root.findall('./configuration/resources/clone') if c.get('id') == clone_id]
if len(clones) != 1:
raise ValueError('Expected exactly one clone with the supplied ID')
for meta in clones[0].findall('meta_attributes'):
if meta.get('id-ref') or meta.find('rule') is not None:
raise ValueError('Clone metadata uses references/rules; inspect manually')
matches = []
for location in root.findall('./configuration/constraints/rsc_location'):
if location.get('rsc') != clone_id:
continue
for rule in location.findall('rule'):
children = list(rule)
if (rule.get('role') == 'Promoted' and rule.get('score') == '200'
and len(children) == 1 and children[0].tag == 'expression'
and children[0].get('attribute') == '#uname'
and children[0].get('operation') == 'eq'
and children[0].get('value') == preferred_node):
# Preserve unrelated rules if the location has more than one.
target = location.get('id') if len(list(location)) == 1 else rule.get('id')
if not target:
raise ValueError('Matching constraint/rule lacks an ID')
matches.append(target)
if len(matches) != 1:
raise ValueError('Expected exactly one simple Promoted/200/#uname preference; inspect the full CIB manually')
target_roles = [p.get('value') for m in clones[0].findall('meta_attributes')
for p in m.findall('nvpair') if p.get('name') == 'target-role']
if target_roles != ['Promoted']:
raise ValueError('Expected one direct clone target-role=Promoted; inspect inheritance/rules manually')
# target-role=Promoted is an intentional production invariant, not a
# temporary node preference. Never remove or rewrite it here.
return [shlex.join(['sudo', 'pcs', 'constraint', 'remove', matches[0]])]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('cib', type=Path)
parser.add_argument('--clone', required=True)
parser.add_argument('--preferred-node', required=True)
parser.add_argument('--lab-validated', action='store_true', help='operator acknowledgement that this candidate passed lab validation')
args = parser.parse_args()
if not args.lab_validated:
parser.error('No rollout commands emitted: lab validation must be completed and acknowledged first')
try:
commands = plan(args.cib, args.clone, args.preferred_node)
except (ValueError, ET.ParseError, OSError) as error:
print('No commands emitted:', error, file=sys.stderr)
return 1
print('# Review against the current CIB before running. No commands have been executed.')
print('\n'.join(commands))
return 0
if __name__ == '__main__':
sys.exit(main())