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
+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())