Skip to content

fix(aws-pcs): stop needrestart from restarting slurmd and killing running jobs - #1165

Merged
KeitaW merged 7 commits into
awslabs:mainfrom
DaisukeMiyamoto:fix/needrestart-slurmd-job-kill
Jul 7, 2026
Merged

KeitaW merged 7 commits into
awslabs:mainfrom
DaisukeMiyamoto:fix/needrestart-slurmd-job-kill

Conversation

@DaisukeMiyamoto

Copy link
Copy Markdown
Collaborator

fix(aws-pcs): stop needrestart from restarting slurmd and killing running jobs

Problem

On the Ubuntu PCS-ready DLAMI, unattended security upgrades (apt-daily-upgrade +
unattended-upgrades) update base libraries such as glibc. needrestart then runs in
automatic mode and restarts every service linked against an updated library. slurmd
links libc, so needrestart restarts slurmd — and restarting slurmd tears down the
slurmstepd steps under it, killing every job running on that node (the job stops and,
if requeued, restarts from scratch, losing in-progress work).

The kill time therefore lines up with apt-daily-upgrade.timer. This is a deterministic
interaction, reproducible on demand: it occurs whenever an unattended upgrade updates a
library slurmd uses.

Two clarifications, because both are easy to assume and both are wrong:

  • It is not a Slurm-package upgrade. slurmd lives under /opt/aws/pcs/... and is not
    apt-managed; nothing upgrades Slurm here.
  • It is not a reboot. Automatic-Reboot is off by default; the job dies solely from
    the slurmd restart.

Fix

Each compute-node-group template (add-cng.yaml and the P5 / P6-B200 / P6-B300 GPU
templates) writes a needrestart drop-in at first boot that excludes slurmd from
automatic restart:

# /etc/needrestart/conf.d/90-pcs-slurm.conf
$nrconf{override_rc} = { qr(^slurmd) => 0 };
  • Security packages still install as before; needrestart still restarts everything else.
    Only the slurmd restart is suppressed (needrestart reports it as deferred), so an
    unattended upgrade can no longer stop a running job.
  • slurmd is the only Slurm systemd service on login/compute nodes — PCS runs the
    controller managed-side, so there is no slurmctld/slurmdbd service to guard. The
    qr(^slurmd) pattern also covers the versioned units (e.g. slurmd-25.11).
  • slurmd picks up the updated libraries the next time it restarts on its own terms (node
    replacement, power-save cycle, manual restart), which is acceptable for PCS, where nodes
    are replaced regularly.

Scope note: the drop-in is a standalone conf.d/*.conf naming only slurmd. If a future
DLAMI or Slurm unit addresses this differently, the file neither conflicts nor errors —
needrestart reads conf.d/*.conf in order and ignores keys it does not use — so it is
safe to keep even after an upstream fix.

Verification

Verified end-to-end on real hardware (PCS clusters, us-east-2), comparing an unmodified
cluster with one carrying this change. On each, a long-running job was started, then a real
apt-get upgrade + libc6 reinstall + needrestart -r a was run on the compute node:

Without the drop-in With the drop-in (this PR)
needrestart action on slurmd restarts it defers it (no restart)
slurmd PID changes unchanged
running job killed survives

Tests

  • aws cloudformation validate-template passes on all assets/*.yaml.
  • tests/lint-docs.sh: PASS.
  • The rendered UserData runcmd step was checked under /bin/sh (dash) — POSIX-clean, no
    bashisms — and the generated 90-pcs-slurm.conf passes perl -c.

Docs

docs/OPERATIONS.md §6.2 documents the interaction and the shipped mitigation.

Files changed

  • architectures/aws-pcs/assets/add-cng.yaml
  • architectures/aws-pcs/assets/add-cng-p5.yaml
  • architectures/aws-pcs/assets/add-cng-p6-b200.yaml
  • architectures/aws-pcs/assets/add-cng-p6-b300.yaml
  • architectures/aws-pcs/docs/OPERATIONS.md

…ning jobs

apt-daily-upgrade updates base libraries (e.g. glibc); needrestart then
auto-restarts every service linked against them. slurmd links libc, so it is
restarted mid-upgrade, which tears down slurmstepd and kills the jobs running
on the node (the job then requeues from scratch). This matched a customer
report of jobs being killed at the same time apt-daily-upgrade.timer fired.

Add a needrestart drop-in via CNG UserData that excludes slurmd/slurmctld/
slurmdbd from automatic restart. Security upgrades still install; only the
Slurm-daemon auto-restart is suppressed, so a running job is never taken down.

Verified end-to-end on real hardware: with the guard, a long-running job
survives an apt upgrade + needrestart that updates glibc; without it, slurmd
restarts and the job is killed/requeued.
Avoid a non-ASCII em-dash in the heredoc that gets written to
/etc/needrestart/conf.d/90-pcs-slurm.conf (rendered as an escaped code point
on disk). Comment-only change; the override_rc line is unaffected.
The needrestart/slurmd job-kill guard was only added to add-cng.yaml. Apply the
same drop-in to the P5/P6-B200/P6-B300 GPU templates, which run the long
distributed-training jobs most affected by an unattended slurmd restart.
…ERATIONS 6.2)

Explain how apt-daily-upgrade -> needrestart -> slurmd restart kills running jobs,
the shipped needrestart drop-in that excludes the Slurm daemons, the heavier
timer-disable alternative, and why the drop-in is forward-compatible.
Rewrite the needrestart/slurmd section from an incident write-up into a note for
users: state plainly that needrestart restarting slurmd stops running jobs, that
the templates already include the mitigation so no action is needed, and that the
drop-in stays harmless (no conflict/error) if the platform fixes this upstream.
slurmctld/slurmdbd are not systemd services on PCS login/compute nodes (PCS runs
the controller managed-side), so excluding them was a no-op. Guard only slurmd,
which qr(^slurmd) also matches for the versioned units (slurmd-25.11 etc.).
Update OPERATIONS 6.2 accordingly.
Condense the needrestart/slurmd note to three short paragraphs and drop the
'disable automatic upgrades entirely' alternative — unnecessary since the
templates already guard slurmd.
@KeitaW
KeitaW self-requested a review July 6, 2026 22:28

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 1/2 — Deployment Pipeline & Operational Correctness (optional nits)

Thanks for this — a well-scoped fix with a genuinely thorough writeup and on-hardware verification. No blocking issues; three low-severity notes below (two inline FYIs on edge-case behavior, one worth acting on).

The four templates have already drifted cosmetically — worth keeping them in lock-step

The guard block is duplicated verbatim across add-cng.yaml, add-cng-p5.yaml, add-cng-p6-b200.yaml, and add-cng-p6-b300.yaml, and the diff already shows two small divergences in add-cng.yaml: it uses an em-dash (—) in the # runcmd runs under /bin/sh (dash) — keep … line where the other three use an ASCII hyphen, and its drop-in comment says Managed by add-cng.yaml UserData where the others say Managed by add-cng UserData. Both are inside # comments so they're harmless today — the payload is identical. The reason to flag it: four hand-maintained copies of the same block is exactly where a load-bearing edit later lands in three of four files. I'd suggest either normalizing add-cng.yaml back to the other three's wording now, or — better long-term — a small CI assertion that the drop-in block is byte-identical across the four templates so drift fails the build.

cat > /etc/needrestart/conf.d/90-pcs-slurm.conf <<'NRCONF'
# AWS PCS: never auto-restart slurmd - restarting it kills
# the jobs running under it. Managed by add-cng.yaml UserData.
$nrconf{override_rc} = { qr(^slurmd) => 0 };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qr(^slurmd) also prefix-matches slurmdbd — no change needed here (FYI)

qr(^slurmd) is an unanchored-prefix regex, so it also matches slurmdbd.service in addition to slurmd and the versioned slurmd-25.11. On PCS login/compute nodes this has no practical effect — as the comment right above correctly notes, slurmd is the only Slurm unit here (the controller and slurmdbd are managed PCS-side). I'd leave it exactly as written: tightening it to qr(^slurmd($|-|\.)) would only guard a unit that isn't present on these nodes. Flagging purely so the prefix-match is a known, deliberate property. It's the same qr(^name) => 0 idiom needrestart ships for dbus/display-managers.

# the jobs running under it. Managed by add-cng.yaml UserData.
$nrconf{override_rc} = { qr(^slurmd) => 0 };
NRCONF
echo "needrestart: slurmd excluded from auto-restart" | tee /var/log/pcs-needrestart-guard.log

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tee writes the success line unconditionally (nit)

The echo … | tee runs regardless of whether the preceding mkdir -p / cat > actually succeeded (no set -e, no && chain), so in the near-impossible failure case (e.g. a read-only /etc at first boot) the log would still read needrestart: slurmd excluded from auto-restart when it wasn't. Cosmetic only — the failure it would misreport isn't a realistic first-boot condition, so I wouldn't hold the PR for it. Mentioning it in case you'd prefer the log line to be strictly truthful (chain the write with && before the echo).

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 2/2 — Things That Look Great & Sources

Things That Look Great

  • The two ways this heredoc could have broken are both handled. (1) Fn::Sub-safe: the whole UserData is Fn::Base64: !Sub |, so any literal ${…} in the body would be read as a CloudFormation variable and fail the template — there's no ${ anywhere in the added text ($nrconf{…} is $ + n, not ${; and = { qr(…) has a space between = and {). (2) Quoted heredoc + de-indentation: <<'NRCONF' stops dash from expanding $nrconf, and the terminator de-indents correctly out of the 14-space YAML literal block so the heredoc actually closes — both must hold, and your perl -c + hardware test show they do.
  • The needrestart mechanism is exactly right — and I re-verified it end-to-end on a fresh box. On a throwaway Ubuntu 24.04 instance (needrestart 3.6, slurm-wlm 23.11): with the drop-in absent, needrestart -r a (after a libc6 reinstall orphans the mapped lib) restarts slurmd (PID changes, listed under Restarting services); with the exact drop-in this PR writes, slurmd moves to Service restarts being deferred and its PID is unchanged — while a cron control is restarted in both runs, confirming -r a still acts. That's precisely the deferred behavior described in the PR. (One scope note for future readers: the downstream job-kill depends on the unit's KillMode; stock Ubuntu's slurmd.service is KillMode=process, so that half is specific to the PCS slurmd unit — which your on-hardware PCS A/B already covers.)
  • The verification is the gold standard for this repo — a real A/B on PCS hardware with an actual apt-get upgrade + libc6 reinstall + needrestart -r a, reporting slurmd PID and job survival as raw before/after rather than a claim.
  • The security tradeoff is stated, not hidden — both the inline comment and OPERATIONS.md §6.2 make explicit that slurmd keeps running against the pre-upgrade libc until its next natural restart, the deliberate cost of the fix.
  • Placement and scope are correct — first runcmd item, a persistent file that covers every future apt-daily-upgrade run; and a standalone conf.d/*.conf naming only slurmd, so a later DLAMI/Slurm change neither conflicts nor errors.

Sources

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The fix is correct, tightly scoped, and independently verified end-to-end: on a fresh Ubuntu 24.04 box, needrestart -r a restarts slurmd without the drop-in and defers it with the exact drop-in this PR writes (a cron control restarts in both runs) — matching the on-hardware PCS A/B in the description. Fn::Sub-safe and POSIX-clean under dash. Only optional nits (see review comments); none blocking.

@KeitaW
KeitaW merged commit ac66712 into awslabs:main Jul 7, 2026
DaisukeMiyamoto added a commit that referenced this pull request Jul 8, 2026
#1168)

* fix(aws-pcs): keep needrestart guard in lock-step across CNG templates

Follow-up to #1165 review:
- normalize add-cng.yaml's guard-block comments to match the other three
  templates (ASCII dash, 'Managed by add-cng UserData')
- add a lint-docs.sh check that the guard block is byte-identical across
  add-cng / add-cng-p5 / add-cng-p6-b200 / add-cng-p6-b300, so a later
  edit that lands in only some of the four hand-maintained copies fails
  the lint
- chain the guard's mkdir/cat with && so the 'excluded from auto-restart'
  log line is only written when the drop-in was actually installed

Verified: lint passes, detects a 1-char divergence when injected; the
chained heredoc block runs clean under dash (and skips the log line
when the write fails); validate-template passes on all four templates.

* chore(aws-pcs): address lint nits from #1168 review

- comment the whitespace-normalization blind spot in guard_extract
  (relative intra-block indentation drift is not detectable)
- printf instead of echo when feeding the failure diff
@DaisukeMiyamoto
DaisukeMiyamoto deleted the fix/needrestart-slurmd-job-kill branch July 8, 2026 14:52
DaisukeMiyamoto added a commit to DaisukeMiyamoto/awsome-distributed-ai that referenced this pull request Aug 24, 2026
…abs#1236 awslabs#5)

$nrconf{override_rc} = { qr(^slurmd) => 0 } reassigns the entire override_rc
hash, discarding needrestart's shipped defaults and leaving slurmd as the
only entry. Mutate the single key instead:
  $nrconf{override_rc}{qr(^slurmd)} = 0;
so the shipped defaults are preserved and only slurmd is added.

Pre-existing since awslabs#1165 (ac66712); this PR carries the one-line fix in
the extracted script rather than deferring it to a separate PR.
KeitaW pushed a commit that referenced this pull request Aug 25, 2026
… lifecycle actions (#1236)

* feat(aws-pcs): extract first-boot logic to scripts, harden all boot fetches

Phase 1 of the node-lifecycle-actions migration: move the needrestart
guard and the FSx OpenZFS//home + Lustre//fsx mounts out of inline
cloud-init runcmd into standalone bash scripts under assets/scripts/,
fetched from the templates bucket like the existing post-install and
directory scripts.

All five boot-script fetches now go through a pcs-fetch helper:
- 3 attempts 15s apart, every attempt logged to /var/log/pcs-boot-fetch.log,
  grep-able 'ERROR: failed to fetch' on final failure (a silent directory
  fetch failure on a replacement login node cost a support round-trip)
- pins the AWS CLI to the classic transfer client via a scoped
  AWS_CONFIG_FILE: on P5/P6 instance types 'auto' resolves to the CRT
  client, which does not follow S3 region redirects, so cross-region
  fetches fail (the root cause of post-install exit 127 on GPU CNGs)
- re-probes the bucket region each attempt; an empty probe falls back to
  the classic client's redirect-following instead of failing

HTTPS fetches (post-install http(s), monitoring GitHub raw) get
curl --retry 3 plus the same ERROR logging.

Verified e2e on us-east-2 deploy-all (login + compute): all fetches
logged, mounts up, Enroot/Pyxis exit 0, OpenLDAP server+client working,
ldap-add-user resolves on both nodes, srun + Pyxis container jobs pass.
Failure path verified: nonexistent key exits 1 after 3 logged attempts.

* feat(aws-pcs): replace CNG UserData with PCS node lifecycle actions

Move ALL first-boot logic from cloud-init UserData to NodeLifecycleActions
on the ComputeNodeGroup resource (requires PCS agent >= 1.5.0-1, in
PCS-Ready DLAMI builds since 2026-07-20 — see docs/PCS-READY-DLAMI.md).
The four CNG templates no longer carry a UserData block at all.

nodeBootstrapped (in order, before slurmd starts):
  1. needrestart-guard      FIRST_BOOT_ONLY / CONTINUE
  2. mount-openzfs-home     EVERY_BOOT / TERMINATE
  3. mount-lustre-fsx       EVERY_BOOT / TERMINATE   (only when Lustre is set)
  4. setup-directory        FIRST_BOOT_ONLY / CONTINUE (only when enabled)
  5. post-install           FIRST_BOOT_ONLY / CONTINUE (only when set)
nodeReady:
  6. install-monitoring     FIRST_BOOT_ONLY / CONTINUE (only when enabled)

Why all-at-once instead of piecemeal: nodeBootstrapped runs only after
cloud-init user-data completes (pcs_bootstrap_finalize), so any UserData
step that depends on a lifecycle-mounted filesystem deadlocks — verified
on a real deploy where directory setup waited 10 minutes for a /home
mount that could not happen until cloud-init exited. Dependencies must
live in one execution domain; within a stage, ordering is guaranteed.

Script interface changes (lifecycle actions pass positional args, not env):
- setup-directory.sh: role/domain-suffix/cluster-id/bucket/prefix as
  args 1-5 (env interface kept); generates the admin password itself
  (SSM reuse logic unchanged); hard-fails unless /home is a mountpoint.
- install-enroot-pyxis.sh: accepts the Slurm version as arg 1
  (PCS_SLURM_VERSION env kept for the custom-AMI build path).
- install-monitoring.sh (new): monitoring installer wrapper — GitHub
  fetch with curl retry, apt dpkg-lock drop-in, 3-attempt install loop.

The agent replaces the hand-rolled fetch plumbing: per-script retries and
logs (/var/log/amazon/pcs/lifecycle/actions/<stage>/<name>.log), and its
downloader is unaffected by the AWS CLI CRT region-redirect bug that
motivated pcs-fetch. Lifecycle config changes via UpdateComputeNodeGroup
now reach existing CNGs with DRAIN semantics instead of requiring CNG
recreation (the LaunchTemplate Version pin problem).

lint-docs.sh: the four-template lock-step check now covers the whole
NodeLifecycleActions block, and every referenced lifecycle script must
exist in assets/scripts/.

Verified e2e on us-east-2 deploy-all (login + compute, directory +
monitoring + Enroot/Pyxis enabled, cross-region templates bucket): all
six scripts exit 0 in order, /home //fsx mounted, slapd + SSSD up,
ldap-add-user resolves on both nodes, Grafana/Prometheus containers
running, srun + Pyxis container jobs pass.

* docs+fix(aws-pcs): align docs with lifecycle actions; keep space-skip working

Template fixes surfaced by the user-impact audit:
- PostInstallScriptUrl skip sentinel: deploy-all passes a single space
  through to add-cng, where the lifecycle ScriptLocation pattern would
  reject it and fail CNG creation. HasPostInstall/HasPostInstallArgs now
  treat empty AND single-space as 'no post-install' (verified: a CNG with
  the skip value creates cleanly with no post-install entry).
- AmiId descriptions (4x add-cng + deploy-all + PARAMETERS.md) now state
  the PCS agent >= 1.5.0-1 floor for pinned/custom AMIs.
- PostInstallScriptUrl/Args descriptions document the new contract:
  https:// only (no plain http), SlurmVersion as first argument,
  PostInstallScriptArgs as a single unsplit argument.

Docs updated for the new mechanics: log paths moved to
/var/log/amazon/pcs/lifecycle/actions/<stage>/<name>.log (README diagram,
OPERATIONS 2.3/4.1/4.2/4.4/6, PARAMETERS, DEPLOY-TESTING, USER-MANAGEMENT
5.2, tests/infra + storage + readme-walkthrough), mount failures now
TERMINATE and replace the node (storage-test troubleshooting), Lustre
tuning hooks reference lifecycle actions instead of UserData.

Pre-PR e2e on a fresh us-east-2 deploy-all (directory + monitoring +
Enroot/Pyxis): all six lifecycle scripts exit 0 on login + compute, no
UserData leftovers on nodes, docs commands run as written (executor grep,
sinfo, docker ps, enroot/pyxis paths, Prometheus targets up, Grafana 200),
srun + Pyxis container job pass, ldap-add-user resolves on both nodes,
skip-sentinel CNG attach validated.

* feat(aws-pcs)!: replace PostInstallScriptUrl/Args with InstallEnrootPyxis

The generic post-install hook existed because PCS had no native way to
run a custom script at first boot. Node lifecycle actions ARE that native
way now, and the only thing this repo ever shipped through the hook is
the Enroot/Pyxis installer — so name the feature for what it does:

- InstallEnrootPyxis ('true'/'false') replaces PostInstallScriptUrl +
  PostInstallScriptArgs on deploy-all and all four add-cng templates.
  The lifecycle entry is named install-enroot-pyxis (its log follows:
  .../nodeBootstrapped/install-enroot-pyxis.log).
- The script location is fixed to
  s3://<S3BucketName>/<S3KeyPrefix>scripts/install-enroot-pyxis.sh —
  dev overrides ride the existing S3BucketName/S3KeyPrefix redirection,
  which removes the last reason pre-merge deploys had to set a URL.
- Defaults preserve prior behavior at both layers: deploy-all 'true'
  (was: empty auto-installs), modular add-cng 'false' (was: empty skips).
  Users with custom post-install scripts add them to the compute node
  group's NodeLifecycleActions directly (documented in the param
  descriptions, README and PARAMETERS.md).
- lint-docs: PostInstallScriptUrl/Args are now BANNED names in docs;
  the empty-vs-space skip-wording check is gone with the sentinel.

BREAKING CHANGE: PostInstallScriptUrl / PostInstallScriptArgs no longer
exist. Set InstallEnrootPyxis=false instead of the single-space sentinel;
attach custom first-boot scripts as node lifecycle actions.

* chore(aws-pcs): publish new lifecycle scripts; lint manifest coverage

The publish manifest is an explicit allowlist — without these entries the
lifecycle-migration templates would reach the production bucket while
their scripts did not, and every post-merge deploy would fail at the
agent's script download (the mount scripts TERMINATE, so nodes would
enter a replace loop). Add the four new scripts:
needrestart-guard.sh, mount-openzfs-home.sh, mount-lustre-fsx.sh,
install-monitoring.sh.

lint-docs.sh now cross-checks every template-referenced lifecycle script
against the manifest so this class of gap fails the publish workflow's
lint step instead of production deploys (verified: removing an entry
makes the lint FAIL).

* docs(aws-pcs): migration notes for the lifecycle-actions changes; final sweep

Add OPERATIONS.md §8 documenting what changes for users coming from the
UserData-based templates (InstallEnrootPyxis replaces PostInstallScriptUrl/
Args, agent >= 1.5.0-1 floor, new log locations, mount failures now replace
the node, installer argument contract) — the repo had no in-tree record of
these behavior changes.

Sweep of README/docs/tests for remaining UserData-era wording: README
architecture diagram now lists all six lifecycle scripts, ROADMAP's FSx-EFA
item targets a lifecycle-action script, OPERATIONS 4.2/6.1 and
infra/storage-test phrasing updated.

* fix(aws-pcs): harden IMDS region lookup in the TERMINATE mount scripts

Under set -euo pipefail, REGION=$(curl ...) took curl's exit status, so a
connection failure killed the script on the assignment line and skipped the
${REGION:?} diagnostic one line below — on an OnError:TERMINATE action the
instance is then replaced before anyone can read the (truncated, error-less)
log. And curl -s (no -f) exits 0 on a 4xx/5xx and captures the error body, so
under HttpTokens=required a throttled token PUT or a 401 GET landed an HTML/401
body in REGION and passed the guard, producing a bogus FSx DNS name and a mount
failure -> terminate.

Add -f (4xx/5xx -> empty), --retry/--connect-timeout/--max-time (ride out a
throttled/slow IMDS), and || true (reach the :? diagnostic instead of dying on
the assignment). Both mount scripts, all three call sites. Verified: a
connection failure now exits 1 with the message instead of a silent exit 7.

Addresses review batch 1/6 (IMDS guard).

* Retry mount in TERMINATE lifecycle scripts (PR #1236 #2)

mount-openzfs-home.sh and mount-lustre-fsx.sh run as OnError:TERMINATE
lifecycle actions with a single-shot mount. The common first-boot NFS
DNS settle race (mount.nfs: Failed to resolve server) fails instantly,
which under TERMINATE replaces the node into the same window. Wrap the
mount in a bounded retry loop (6 attempts, 10s backoff) verified with
mountpoint(8), so TERMINATE fires only on a persistent failure.

* Constrain FSx filesystem ID params with AllowedPattern (PR #1236 #3)

FSxOpenZFSFilesystemId feeds the unconditional mount-openzfs-home action
(OnError:TERMINATE) with no AllowedPattern/Default. An empty or malformed
value passes stack create, then the mount script fails at boot and the
node is TERMINATEd and replaced into the same bad value -- an endless
terminate/replace loop. Add a mandatory '^fs-[0-9a-f]{8,17}$' pattern so
CFN rejects it at create time. Add the sibling '^$|^fs-...$' pattern to
FSxLustreFilesystemId, whose empty value is the documented 'no /fsx'
opt-out (HasLustre). Applied identically across all 4 add-cng templates.

* Constrain MonitoringRepo/MonitoringVersion with AllowedPattern (PR #1236 #4)

Both feed the raw.githubusercontent.com URL that install-monitoring.sh
fetches and bash-runs. These are admin-only params, so the AllowedPattern
is input hygiene (catch typos at CFN validation, match the AmiId/FSx-id
convention) rather than an anti-exploit measure.

MonitoringRepo: ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ (exactly owner/repo).
MonitoringVersion: ^[A-Za-z0-9._/-]+$ -- deliberately allows '/' so the
documented fork+branch testing workflow (feat/... branches) keeps working;
rejects whitespace and shell metacharacters. Applied across all 4
add-cng templates.

* Set only the slurmd needrestart override, not the whole hash (PR #1236 #5)

$nrconf{override_rc} = { qr(^slurmd) => 0 } reassigns the entire override_rc
hash, discarding needrestart's shipped defaults and leaving slurmd as the
only entry. Mutate the single key instead:
  $nrconf{override_rc}{qr(^slurmd)} = 0;
so the shipped defaults are preserved and only slurmd is added.

Pre-existing since #1165 (ac66712); this PR carries the one-line fix in
the extracted script rather than deferring it to a separate PR.

* Update OPERATIONS §6.2 needrestart snippet to match the #5 fix

The doc code sample still showed the whole-hash reassignment; align it with
the one-key form now written by needrestart-guard.sh.

* Document the stack-update upgrade path in OPERATIONS §8 (PR #1236 M1)

Two gaps the review flagged:
- §8 said existing stacks keep working as-is but never said the update
  itself DRAINs and replaces the whole fleet (launch-template version bump
  + NodeLifecycleActions change). Added a fleet-cycle note.
- A custom-bucket stack has none of the four scripts this revision adds;
  updating it fails mount-openzfs-home (TERMINATE) on every replacement =
  replace loop to an empty cluster. Added a sync-before-update prerequisite
  linking DEPLOY-TESTING §2.

Also: fixed the mount-row debug advice (a fleet-wide failure leaves no
surviving node; point to OnError:STOP_SEQUENCE / configure-cloudwatch-logs.sh),
noted the load-bearing S3 read in the PARAMETERS S3BucketName/S3KeyPrefix
rows, and framed the DEPLOY-TESTING sync as an update prerequisite.

* Remove the unused S3BucketName=local template sentinel (PR #1236 M2)

UseLocalTemplates (S3BucketName='local') switched nested-stack TemplateURLs
to relative paths for an aws cloudformation package local-dev flow. It is
undocumented, has no reference in docs/tests, and dates to the first
reference-cluster commit -- never exercised by this project's workflows,
which sync to a real bucket. It is also now a trap: the sentinel is
forwarded verbatim to the CNG child stacks as their script bucket, so boot
scripts resolve to a literal s3://local/... and fail; with the mounts now
load-bearing (OnError:TERMINATE) that terminates the node instead of
degrading gracefully. cfn package rewrites nested TemplateURLs but not the
runtime script fetch, so 'local' cannot supply boot scripts by design.
Delete the condition and collapse all 7 TemplateURLs to the S3 URL form.

* Verify LDAP admin bind before writing SSM in setup-directory (PR #1236 M3)

debconf-set-selections seeds slapd's olcRootPW only when apt-get actually
installs slapd. If slapd is already present, the install is a no-op and slapd
keeps its previous admin password, but the script still overwrote SSM with the
newly configured password and swallowed the failing OU-creation binds
(2>/dev/null || true) -- a false success leaving a stored credential that does
not bind. Add an ldapwhoami check after slapd is up: on bind failure, log
loudly and return before creating OUs or overwriting SSM, so a working stored
credential is never clobbered by a non-binding one. Directory action is
OnError:CONTINUE, so this logs and the node continues.

* fix(pcs): match whole fstab line and drop stray mount arg in mount-openzfs-home

grep -qF is a substring match: a commented-out /home fstab line (added by an
operator debugging a hung mount) satisfies the guard, so the active entry is
never re-added. mount -a then no-ops, the script reports success, and the
stash is restored onto — then deleted from — a local /home that the next boot
silently shadows. Use grep -qxF (whole-line match) on both guards so a
commented line no longer counts as present.

Also drop the stray 'defaults' positional from 'mount -a -t nfs defaults'
(introduced with the mount retry loop); mount -a takes no such argument.

* fix(pcs): don't let a failed /fsx chmod terminate a healthy node

mount-lustre-fsx.sh writes no fstab entry, so it re-mounts and re-chmods on
every boot (EVERY_BOOT). Under set -e a failing 'chmod 1777 /fsx' exits
non-zero even when the mount at the previous line succeeded, so OnError:
TERMINATE destroys a node whose /fsx is fine. A chmod failure here is a
shared-side condition (root_squash / read-only FSx / MDS hiccup), never
node-local — terminating just replaces the node into the same failure
(launch/drain/replace loop). Decouple the chmod from the exit status and warn
loudly instead, keeping the healthy node.

Leaves the every-boot chmod itself in place (a safe first-boot-only guard on
shared Lustre is non-trivial given .lustre/lost+found); tracked in review reply.

* docs(pcs): drop removed empty/single-space InstallEnrootPyxis semantics

InstallEnrootPyxis is now AllowedValues ['true','false']; empty and single-space
are no longer legal. Rewrote the three passages that still described them
(OPERATIONS.md 2.1, CUSTOM-AMI.md, PARAMETERS.md 5.3 preamble), naming the
per-template default (deploy-all 'true', add-cng* 'false'). Renamed the
PARAMETERS.md 5.3 heading and the README console label to the actual console
group 'Container Runtime (Enroot/Pyxis)', restoring the mirror-the-console
invariant. The OPERATIONS.md 8 migration row keeps its single-space mention
(correct: it explains the old behavior when migrating off PostInstallScript*).

* feat(pcs): default InstallEnrootPyxis=true in the standalone add-cng templates

The four add-cng*.yaml defaulted InstallEnrootPyxis to 'false' while
deploy-all defaults 'true', so the documented one-click path for adding a
queue to an existing cluster (README Launch Stack buttons) produced nodes with
no container runtime — srun --container-image would fail on the new queue
only, while the README says the runtime is on by default.

Rather than paper over the split with per-template scoping in the docs, make
the default consistent: the container runtime is a headline behavior of this
ML reference architecture, and the installer is idempotent (a fast no-op when
pre-baked into AmiId). Deploy-all already passes the value down explicitly, so
this only changes the standalone add-cng* path. Set 'false' to opt out.

Simplifies the docs that had to name the per-template default (OPERATIONS 2.1,
CUSTOM-AMI); README's unscoped 'on by default' is now accurate everywhere.
All four templates validate; docs lint passes.

* docs(pcs): update in-file comments crediting UserData for the moved interface

Five comments still described the pre-PR UserData env interface, one made false
by this PR's own change (setup-directory.sh:30 said LDAP_ADMIN_PASSWORD is
'auto-generated by UserData' though the script now generates it itself). Swept
all five to describe the current interface: the lifecycle action passes values
positionally (env vars remain for manual / custom-AMI runs), and SlurmVersion
arrives as $1 with PCS_SLURM_VERSION as the fallback.

* test(pcs): widen manifest cross-check to scripts fetched by other scripts

The manifest guard grepped assets/add-cng*.yaml only, so ldap-add-user.sh —
fetched at boot by setup-directory.sh, not via a template ScriptLocation — was
invisible to it. Dropping its manifest entry left both lint-docs.sh and the
staging script green while the object never reached the production bucket, so
the login node's aws s3 cp fails and the cluster comes up without the
ldap-add-user.sh helper USER-MANAGEMENT.md documents (silent degrade).

Scan assets/*.yaml and assets/scripts/*.sh so boot-time fetches from other
scripts are covered too. Verified: normal run still PASS (7/7 present and in
the manifest, no false positives); dropping the ldap-add-user.sh entry now
FAILs as intended.

* fix(pcs): don't silently rotate the LDAP admin password on an SSM read error

setup-directory.sh read the stored admin password with
'aws ssm get-parameter ... 2>/dev/null || echo ""', collapsing every failure
(ParameterNotFound, AccessDenied, throttling) into "nothing stored yet". A
transient read failure during a login-node replacement therefore regenerated
the password, reconfigured slapd, and overwrote SSM with --overwrite while the
user DB on /home/ldap-db persisted — the exact silent rotation the reuse block
exists to prevent.

Capture the exit status (set -e-safe if/else) and branch: reuse a returned
value; keep the freshly generated password only on rc==0-empty or a genuine
ParameterNotFound; on any other error, log loudly and return 1 rather than
regenerate against the persistent DB. The directory action is OnError:CONTINUE,
so the node continues (log-and-continue), consistent with the ldapwhoami bind
check added earlier for the debconf-only-on-fresh-install case.

* docs(pcs): drop non-working 'latest' from MonitoringVersion guidance

raw.githubusercontent.com resolves only real tag/branch refs, so a
MonitoringVersion of 'latest' 404s the post-install.sh fetch and (under
OnError:CONTINUE) leaves monitoring silently uninstalled. Remove 'latest'
from the parameter Description / ConstraintDescription across all five
templates and from PARAMETERS.md; the default stays a real tag (v2.10.2).

* fix(pcs): harden mount + monitoring boot scripts, fix doc anchors

mount-openzfs-home.sh: keep exactly one active /home fstab entry via
ensure_home_fstab_entry (mountpoint-field match, rewrite in place),
tolerate rsync exit 23/24 via safe_rsync so a benign ACL/vanished-file
code can't trip set -e into TERMINATE, and mount /home specifically
instead of mount -a so an unrelated custom-AMI NFS entry can't fail the
action.

install-monitoring.sh: fetch post-install.sh into a mktemp -d workdir
(no fixed /tmp path a dispatched job could race), reconcile the role
header (login=server, compute=exporters), note curl --retry doesn't
retry HTTP 4xx, scope the apt lock-timeout drop-in comment, and chmod
600 the upstream install log to close the Grafana-password exposure on
the multi-user login node.

docs: fix the 3.1 cross-file anchor double-hyphen (README, PARAMETERS)
and note that under CACHE_ONCE a re-sync needs an instance replacement
to reach a running node (DEPLOY-TESTING).

* fix(pcs): preserve existing /home dir modes on first-boot restore

The first-boot /home restore rsynced the node-local snapshot back over the
freshly mounted shared export with `-aA --ignore-existing`. For a directory
that already exists on the shared side (e.g. an admin-tightened /home/ubuntu),
rsync still applied the snapshot's mode/times to it, silently loosening the
shared directory's permissions on the next new node's first boot. Add
`--no-perms --omit-dir-times` so the restore only fills in missing files and
never rewrites the mode/times of a directory that already exists on the export.
`--ignore-existing` still protects existing shared files. (PR #1236 review #3)

Also:
- docs(OPERATIONS §8): note the standalone add-cng*.yaml default flip to
  InstallEnrootPyxis=true (deploy-all already defaulted true) so migrating
  stacks that relied on the old `false` default aren't surprised.
- docs(README): fix the Test 9 cross-file link (heading lives in
  tests/hpc-efa-test.md, not tests/README.md).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants