Freeze lab-tested PostgreSQL plugin engine

Extract PostgreSQL, Podman runtime and systemd lifecycle adapters while preserving existing lifecycle and scoring behavior. Update RPM layout and add guarded operator-run lab harnesses.

Record successful manual stop/start, promotion, demotion and rewind-only rejoin with pinned installed hashes. The unfenced lab does not validate automatic failover, Pacemaker-managed transitions or divergent-data rewind.

Validation: 103 offline tests, original-function differential checks, checksum manifest and whitespace checks passed.
This commit is contained in:
Lyncoln
2026-09-13 01:57:04 -04:00
parent 488a1672d7
commit ec2077f979
41 changed files with 1925 additions and 331 deletions
+210
View File
@@ -0,0 +1,210 @@
# Podman plugin engine: first extraction
Development baseline: freeze commit `488a167`. This branch extracts the existing PostgreSQL/Podman/systemd implementation.
It is installed on DAVID-ELITEDESK in the lab only; see lab-plugin-basic-test.md. Production
hash provenance is still pending as described in testing.md. Quadlet, bootstrap and cluster configuration remain unchanged. The lab RA files
were replaced, and basic monitor/start checks refreshed standby scoring without
restarting PostgreSQL. No production installation has occurred.
## Scope and supported configuration
The default and only supported combination is:
```text
application=postgresql
container_runtime=podman
lifecycle_manager=systemd
```
These are advertised OCF instance parameters, not Pacemaker meta-attributes.
Existing resource configurations need no new parameters. Missing or empty
selectors use the defaults. Explicit unsupported values, including Docker or
direct-runtime lifecycle, fail with OCF_ERR_CONFIGURED (6) before operations.
The entrypoint/provider and every existing parameter default remain unchanged.
The engine loads a fixed allowlist of packaged files beside the entrypoint.
There is no arbitrary plugin pathname, dynamic shell evaluation, network plugin
installation, or fallback to another runtime. Every implementation declares API
version 1; missing files, incompatible versions and missing required hooks fail
configuration. Packaged code is trusted/root-owned, as was the original RA.
## Boundaries
```text
Pacemaker OCF action
-> shared scoring/action wrappers
-> applications/postgresql/plugin.sh
-> existing PostgreSQL lifecycle and recovery modules
-> runtimes/podman.sh (inspect / exec / info)
-> lifecycles/systemd.sh (user service lifecycle)
-> unchanged as_service_user privilege boundary
```
`engine.sh` selects and checks plugins and adds selector metadata. `common.sh`
contains the existing score-publication and action wrappers, now calling the
application interface. `container.sh` retains service-user access plus
compatibility health helpers that delegate to the selected adapters.
`applications/postgresql/plugin.sh` owns the existing PostgreSQL defaults, metadata,
validation and seven PostgreSQL/HA modules. Application modules and the state
helper live together under
`applications/postgresql/`; runtime and lifecycle adapters occupy their own
`runtimes/` and `lifecycles/` directories. The loader uses fixed relative paths.
Future applications will have their own
metadata and dependency validation rather than inherit PostgreSQL assumptions.
The packaged layout is:
```text
managed-container.d/
engine.sh
common.sh
container.sh
applications/postgresql/
plugin.sh
postgres.sh, auth.sh, replication.sh, notify.sh
promote.sh, demote.sh, rejoin.sh
promotion_state.py
lifecycles/systemd.sh
runtimes/podman.sh
```
The PostgreSQL plugin sets its own `MC_APPLICATION_DIR` for sibling modules and
the Python state helper. Host evidence remains at its existing `/run` location;
only packaged code paths changed. The public RA entrypoint is unchanged.
## Interface contract, API 1
All interfaces are sourced Bash functions in a single RA invocation. Arguments
are passed as arrays using `"$@"`; adapters do not concatenate or evaluate command
strings. Existing PostgreSQL shell snippets remain explicit arguments to `sh -c`;
this extraction does not redesign or broaden their trusted configuration inputs.
| Interface | Contract |
| --- | --- |
| mc_runtime_exec ARGS... | Execute in the container through the service owner; preserve stdout, stderr, and exact exit status. |
| mc_runtime_exec_bounded DURATION ARGS... | Same execution with timeout placed inside the service-user boundary; retain kill-after=1s. Current scoring callers pass 3s. |
| mc_runtime_inspect ARGS... | Read container properties through the same runtime/user boundary. |
| mc_runtime_info ARGS... | Validate runtime access through the service owner. |
| mc_lifecycle_start SERVICE | Start the configured user-systemd service. |
| mc_lifecycle_stop SERVICE | Stop that service, preserving failures. |
| mc_lifecycle_running SERVICE | systemctl --user is-active --quiet. |
| mc_lifecycle_cat SERVICE | Inspect service definition for existing validation. |
| mc_application_monitor | Return the application's normalized OCF health/role code. |
| mc_application_standby_score | Emit the eligible standby score, or fail; PostgreSQL owns evidence and freshness. |
| mc_application_action ACTION | Dispatch only start/stop/promote/demote/rejoin to the existing application functions. |
| mc_application_validate | Run existing application/configuration validation. |
| mc_application_metadata | Return application metadata without runtime or CIB access. |
| mc_application_notify | Process the existing transition notification. |
The runtime exec argument grammar currently follows the existing callers:
`--user USER`, optional `--env KEY=VALUE` entries, container name, command and
arguments. Inspect retains the existing Go-template argument. This is a narrow
Podman adapter contract, not a claim that every future runtime accepts it.
A second runtime must explicitly implement and test those semantics.
## Normalization and state
For this extraction the existing PostgreSQL monitor is the normalizer: it turns
PostgreSQL's role/health observations into OCF codes. No extra JSON protocol or
new independently maintained role state is introduced.
| Observation | Monitor code |
| --- | ---: |
| Healthy standby | 0 (Unpromoted) |
| Healthy primary | 8 (Promoted) |
| Container absent/stopped, or PostgreSQL stopped | 7 (not running) |
| PostgreSQL unready or failed/invalid role query | 1 (generic error) |
Container availability, application health/role and promotion eligibility remain
separate concepts. Container-running/PostgreSQL-stopped is a legitimate
maintenance condition, but is not OCF start success. A healthy standby can have
score -INFINITY without losing its Unpromoted role report. Core score validation
still replaces malformed/failed score output with -INFINITY.
All existing scores, freshness checks, persisted identity/LSNs, timeout locations,
notification handling, fencing assumptions, standby-only start and manual fenced
rejoin rules are preserved. Direct application-plugin calls are internal;
Pacemaker dispatch continues through the score/lifecycle wrappers.
## Maintenance and lifecycle ownership
Quadlet remains responsible for the bootstrap entrypoint, `Exec=sleep infinity`,
`RunInit=true`, stop signal/timeout, mounts, image, environment and systemd service
policies. `SuccessExitStatus=143` is systemd's classification of its managed
service; the runtime exec adapter must still return 143 as an application-command
failure. `Restart=on-abnormal` remains solely in the existing unit. No new
restart loop, container creation, container replacement or supervision is added.
During demote/rejoin, PostgreSQL may be stopped while its container remains
available. Ordinary stop still stops PostgreSQL before asking systemd to stop
the container service. These ordering rules remain in the PostgreSQL lifecycle
functions; the adapters do not reorder them.
## Preservation and validation
The original 29 functions are retained by name. Some now call adapter functions,
so their literal hashes differ. `tests/verify_refactor.py` reverses only the
explicit command-boundary substitutions and compares every remaining function
byte with the immutable reference. It also retains all 22 differential lifecycle
cases and verifies original metadata plus the four accumulated added parameters
(freshness plus three selectors). Existing scoring tests use the actual engine
loader and still exercise the original handlers against fake external boundaries.
`tests/test_plugin_engine.py` checks argument fidelity (including quotes, spaces,
newlines and literal shell syntax), lifecycle ownership, exit-code propagation,
timeout placement, application dispatch, metadata discovery, missing/incompatible
plugins, and unsupported/path-like selections. It also checks that PostgreSQL
modules no longer invoke Podman/systemd or service-user execution directly.
Run:
```sh
python3 tests/verify_refactor.py
python3 tests/test_plugin_engine.py
python3 tests/test_promotion_scores.py
python3 tests/test_disconnected_state.py
python3 tests/test_promotion_scheduler.py
python3 tests/test_promotion_cleanup.py
python3 tests/verify_promotion_sql.py
```
The last command uses only its own disposable PostgreSQL container. RPM checks
include the plugin tests and verify discovery from the staged installation and
an unrelated working directory. No live HA transitions are part of this branch's
validation. Live lab authorization and actual configuration comparison remain
necessary before deployment.
## Follow-on work
After this extraction passes lab validation, a separate change can add direct
Podman lifecycle management for pre-created maintenance containers. That work
must define exit/restart behavior and sole lifecycle ownership. Container creation,
RA-coordinated standby provisioning, a second runtime or another application are
separate extensions. The current PostgreSQL plugin is intentionally still the
only supported application; this is not yet a general multi-application framework.
## Results for this extraction
Passed: 22 original differential lifecycle cases, 25 scoring tests, 16 state/monitor
tests, 11 scheduler tests, 4 preference-planner tests, 11 engine contract tests,
and all 9 PostgreSQL 18 SQL scenarios. Fedora 43 RPM build, disposable installation,
15-file ownership/mode/payload checks, and the installed state-helper CLI passed.
Folder-layout build log: `/tmp/mc-plugin-folders-rpm.log`; development artifacts:
`dist/plugin-folders/`. Those build and offline checks did not run live HA transitions; subsequent
operator-run lab transitions are recorded below.
The core still retains the existing score vocabulary and freshness-parameter
validation for this sole supported PostgreSQL profile. API 1 is an internal
extraction interface, not a stable third-party plugin SDK. Generalizing these
remaining policy assumptions should be driven by a second real application,
not assumed complete by this milestone.
Basic ELITEDESK installation checks subsequently passed with score 100; all 15
installed files match the candidate. A controlled ELITEDESK standby stop/start subsequently passed according to
operator-supplied output: stopped score -INFINITY, resumed streaming score 100,
same database and new postmaster. The operator subsequently reported successful controlled switchovers in both
directions and a rewind-only rejoin on ELITEDESK; see [role test results](lab-role-tests.md).
Fencing, Pacemaker-managed transitions and divergent-data rewind remain untested
with this extraction.
See [the lab record](lab-plugin-basic-test.md) for evidence attribution.