Skip to content

feat: add runtimeRequiredCordonAfter to support persistent node cordons - #401

Open
natherz97 wants to merge 2 commits into
NVIDIA:mainfrom
natherz97:runtime-required-cordon
Open

feat: add runtimeRequiredCordonAfter to support persistent node cordons#401
natherz97 wants to merge 2 commits into
NVIDIA:mainfrom
natherz97:runtime-required-cordon

Conversation

@natherz97

Copy link
Copy Markdown

Description

This PR adds a runtimeRequiredCordonAfter field to the NodeWright API to support persistent node cordons. This PR implements the behavior outlined in the following comment: #284 (comment).

Feature overview:

  • The persistent node cordon is applied if any runtimeRequired NodeWright CR enables runtimeRequiredCordonAfter. If any NodeWright CR sets runtimeRequired to false but runtimeRequiredCordonAfter to true, the cordon will not be applied.
  • The cordon is only applied at the same time as taint removal if the taint currently exists on the node. If the taint has already been removed from a node, the cordon is not applied even if a new runtime-required NodeWright targets it.
  • The cordon applied by runtimeRequiredCordonAfter will persist through any future NodeWright CR executions with interrupts that may require cordoning the node (which themselves may have runtimeRequired set to true). This is accomplished by using a runtimeRequiredCordon annotation which ensures the cordon is preserved after the interrupt package completes. Any pre-existing cordon applied outside NodeWright is not preserved, which is consistent with current behavior.
  • Releasing this persistent cordon requires removing the runtimeRequiredCordon annotation and setting unschedulable to false in the same patch.

Closes #284

Checklist

  • I am familiar with the Contributing Guidelines.
  • My commits are signed off (git commit -s) per the DCO.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@natherz97
natherz97 requested a review from a team August 3, 2026 21:01
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Welcome to NodeWright, @natherz97! Thanks for your first pull request.

Before review, please ensure:

  • All commits are signed off per the DCO (git commit -s)
  • Commits follow Conventional Commits
  • CI checks pass (tests, lint, security scan)
  • The PR description explains the why behind your changes

A maintainer will review this soon.

@github-actions github-actions Bot added doc Documentation change (PR path label; doc issues use the Documentation type) component/operator Skyhook operator (controller-manager) component/ci CI workflows, GitHub Actions, and repo tooling component/tests End-to-end / chainsaw test suites (k8s-tests) labels Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 1c81c812-1ff1-49cb-bc52-a315e1a6faf8

📥 Commits

Reviewing files that changed from the base of the PR and between 231c020 and 9092944.

📒 Files selected for processing (16)
  • docs/runtime_required.md
  • k8s-tests/chainsaw/nodewright/runtime-required-cordon-after/chainsaw-test.yaml
  • operator/api/nodewright/v1alpha1/nodewright_types.go
  • operator/api/v1alpha1/nodewright_conversion.go
  • operator/api/v1alpha1/nodewright_conversion_test.go
  • operator/api/v1alpha1/skyhook_types.go
  • operator/config/crd/bases/nodewright.nvidia.com_deploymentpolicies.yaml
  • operator/config/crd/bases/nodewright.nvidia.com_nodewrights.yaml
  • operator/config/crd/bases/skyhook.nvidia.com_deploymentpolicies.yaml
  • operator/config/crd/bases/skyhook.nvidia.com_skyhooks.yaml
  • operator/config/rbac/role.yaml
  • operator/config/webhook/manifests.yaml
  • operator/internal/controller/skyhook_controller.go
  • operator/internal/controller/skyhook_controller_test.go
  • operator/internal/wrapper/node.go
  • operator/internal/wrapper/node_test.go
💤 Files with no reviewable changes (4)
  • operator/config/rbac/role.yaml
  • operator/config/crd/bases/skyhook.nvidia.com_deploymentpolicies.yaml
  • operator/config/crd/bases/nodewright.nvidia.com_deploymentpolicies.yaml
  • operator/config/webhook/manifests.yaml

📝 Walkthrough

Walkthrough

The change adds runtimeRequiredCordonAfter to NodeWright and Skyhook specifications and CRD schemas. Runtime-required completion can now remove the runtime-required taint, add a cordon annotation, and mark the node unschedulable. Node-wrapper logic preserves this cordon during uncordon operations. Unit, integration, and Chainsaw tests cover the behavior. Documentation describes configuration and manual release.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: lockwobr, ayuskauskas

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Several manifest files contain unrelated removals of standalone license-header comment lines. Remove the unrelated license-header cleanup changes from the pull request, unless they are required by an established repository-wide formatting rule.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the addition of runtimeRequiredCordonAfter and its persistent node-cordon behavior.
Description check ✅ Passed The description explains the new field, cordon behavior, persistence, release workflow, testing, and documentation updates.
Linked Issues check ✅ Passed The implementation satisfies issue #284 by cordoning tainted nodes during taint removal, preserving the cordon, and supporting external uncordoning.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/runtime_required.md`:
- Around line 62-63: Update the runtime-required taint-removal documentation
around the `runtimeRequiredCordonAfter` behavior to state that removing the
taint removes only that scheduling barrier. Clarify that the node becomes
schedulable only when no `nodewright.nvidia.com/cordon_*` owners remain, since
other NodeWright or external cordons may still keep it unschedulable.

In
`@k8s-tests/chainsaw/nodewright/runtime-required-cordon-after/chainsaw-test.yaml`:
- Around line 107-113: Extend the release-cordon phase after the kubectl patch
to wait for reconciliation, then add a final assertion verifying that node
kind-worker no longer has the runtimeRequiredCordon annotation and that
spec.unschedulable is false. Use the existing Chainsaw assertion conventions in
this test.

In `@operator/config/crd/bases/skyhook.nvidia.com_skyhooks.yaml`:
- Around line 630-638: Regenerate the legacy CRD schema so the
runtimeRequiredCordonAfter description matches the complete documentation in the
Skyhook API type, using the repository’s standard make manifests generate
command after the API change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 4968e5ff-4b5b-4194-a699-2d1caa66f381

📥 Commits

Reviewing files that changed from the base of the PR and between e5a3ab6 and c0fa965.

📒 Files selected for processing (16)
  • docs/runtime_required.md
  • k8s-tests/chainsaw/nodewright/runtime-required-cordon-after/chainsaw-test.yaml
  • operator/api/nodewright/v1alpha1/nodewright_types.go
  • operator/api/v1alpha1/nodewright_conversion.go
  • operator/api/v1alpha1/nodewright_conversion_test.go
  • operator/api/v1alpha1/skyhook_types.go
  • operator/config/crd/bases/nodewright.nvidia.com_deploymentpolicies.yaml
  • operator/config/crd/bases/nodewright.nvidia.com_nodewrights.yaml
  • operator/config/crd/bases/skyhook.nvidia.com_deploymentpolicies.yaml
  • operator/config/crd/bases/skyhook.nvidia.com_skyhooks.yaml
  • operator/config/rbac/role.yaml
  • operator/config/webhook/manifests.yaml
  • operator/internal/controller/skyhook_controller.go
  • operator/internal/controller/skyhook_controller_test.go
  • operator/internal/wrapper/node.go
  • operator/internal/wrapper/node_test.go
💤 Files with no reviewable changes (4)
  • operator/config/crd/bases/skyhook.nvidia.com_deploymentpolicies.yaml
  • operator/config/rbac/role.yaml
  • operator/config/webhook/manifests.yaml
  • operator/config/crd/bases/nodewright.nvidia.com_deploymentpolicies.yaml

Comment thread docs/runtime_required.md
Comment thread operator/config/crd/bases/skyhook.nvidia.com_skyhooks.yaml
@natherz97
natherz97 force-pushed the runtime-required-cordon branch 2 times, most recently from 315d32b to 231c020 Compare August 3, 2026 21:24
@lockwobr

lockwobr commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

@natherz97 I think maybe there is a release happening for the base image. I think you might just had bad luck. 4.0.1 does not yet exist. If we wait this should resolve itself, in the meantime i will talk to that team to see if we can fix this race condition in the base container publishing.

@ayuskauskas ayuskauskas 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.

Chart CRDs not updated

chart/templates/nodewright-crd.yaml and chart/templates/skyhook-crd.yaml don't include the new runtimeRequiredCordonAfter field. Users deploying via Helm won't be able to set the field — it will be silently dropped by the apiserver webhook. Per repo convention, operator/config/ and chart/ must stay in sync in the same PR.


Minor
operator/api/v1alpha1/skyhook_types.goRuntimeRequiredCordonAnnotation constant

This constant value (skyhook.nvidia.com/runtimeRequiredCordon) is never written to any node. The controller only imports api/nodewright/v1alpha1, so all nodes get nodewright.nvidia.com/runtimeRequiredCordon regardless of whether they were targeted by a legacy Skyhook CR or a NodeWright CR. After conversion, there are no legacy Skyhook CRs left in the reconciler — it always works against the converted nodewright type.


kubectl nodewright reset doesn't clear the persistent cordon

resetAnnotationKeys() in operator/cmd/cli/app/reset.go doesn't include RuntimeRequiredCordonAnnotation. After a kubectl nodewright reset, the package state is wiped and the NW re-runs, but the persistent cordon (unschedulable + annotation) remains. This creates a confusing intermediate window where the node is cordoned but has no runtime-required taint. More importantly, if the CR is deleted and recreated without runtimeRequiredCordonAfter, the cordon persists indefinitely with no self-healing path. Either add the annotation to resetAnnotationKeys() (and uncordon if no other Skyhook cordon holds it), or document this as intentional.


docs/runtime_required.md — two gaps

  1. kubectl nodewright reset behavior: the persistent cordon currently survives a reset (see above). Worth calling out here so users aren't surprised when they reset and find the node still cordoned.
  2. REAPPLY_ON_REBOOT=true interaction: the autoTaintNewNodes section already documents that a boot-ID change re-applies the taint and triggers a re-run. With runtimeRequiredCordonAfter: true, the cordon will be re-applied after that re-run completes. That is probably the intended behavior, but it should be stated explicitly.

k8s-tests/chainsaw/nodewright/runtime-required-cordon-after/chainsaw-test.yaml — multi-CR coverage

The test exercises single-CR behavior, but the PR description says 'the cordon is applied if any runtimeRequired NodeWright enables runtimeRequiredCordonAfter.' The unit test runtimeRequiredCordonAfterEnabled should return true when any skyhook has it enabled covers this at the unit level. Consider adding a step (or a separate test) where two NodeWrights target the same node — one with runtimeRequiredCordonAfter: true, one without — to give e2e confidence for the 'any' semantic.

@lockwobr lockwobr 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.

Cross-review summary

Automated multi-reviewer analysis of 9323a0bd, cross-reviewed to consensus with adversarial verification of every finding. Nothing was run against a cluster and no tests were executed; every claim comes from reading the pinned commit. CI is green on this commit (all 36 checks, including all four e2e pools across k8s 1.33-1.36 and helm-tests).

This is scoped to what is not already covered by the existing review and the automated comments on this PR. The chart-CRD gap and the chainsaw release assertion are both already raised, so they are not repeated here.

The operator-side implementation is coherent and well tested: the taint removal and the cordon land in a single patch, taints.RemoveTaint deep-copies so the merge base is unmutated, and the taint-exists guard makes a repeat reconcile a no-op. Everything below is at a boundary.

One addition to the chart-CRD point

Agreed, and one thing worth adding: the e2e in this PR structurally cannot catch it, because make e2e-tests installs CRDs from operator/config via kustomize, not from the chart. So the test exercises a schema chart users never receive. If this class of gap is worth closing permanently, a helm-tests case asserting a new field survives an apply round trip would do it, since the hand-maintained chart mirror is easy to miss.

A doc page not yet mentioned

docs/uninstall.md:88 states "A node is uncordoned only once no cordon_* ownership annotations remain (other NodeWrights sharing the node may still hold it); the CR finalizer is removed last." That is now false: the finalizer path leaves the node unschedulable even when zero cordon_* annotations remain, because the unsuffixed runtimeRequiredCordon key gates Uncordon and CleanupSCRMetadata cannot remove it. Line 233 compounds it by pointing users at a reset command that does not clear it either.

docs/interrupt_flow.md:88 has the same problem for a different reason; see the inline comment on wrapper/node.go. A fix scoped to one page would leave the other asserting the opposite of the new behavior.


Open questions

  • Is stranding the cordon after NodeWright deletion intentional? Framing release as an external-actor action is coherent, but helm uninstall deletes all NodeWrights via the cleanup-skyhooks-job pre-delete hook, and after that no controller exists that could ever clear the annotation. Every affected node stays unschedulable with no self-healing path. This is the hinge for the wrapper/node.go finding.
  • Should the cordon be re-converged level-triggered? Detail in the inline comment on skyhook_controller.go.
  • Should the webhook reject runtimeRequiredCordonAfter: true with runtimeRequired: false? Silently ignored today, documented only in prose. Worth noting that neither runtimeRequired nor autoTaintNewNodes carries a validation rule either, so omitting one is consistent with existing convention.
  • Should kubectl nodewright node status surface this cordon? cordonAnnotationPrefix is declared in node_status.go:42 but never used for display, so the CLI shows no cordon state at all. New surface rather than a regression, but a node held unschedulable by an annotation the CLI cannot show is a rough edge.

Checked and clear

Two things that look like problems on a skim and are not, recorded so nobody re-derives them:

  • No import-alias mismatch. RuntimeRequiredCordonAnnotation is declared in both api packages from each package's own METADATA_PREFIX, so it resolves to two different strings. Both skyhook_controller.go:35 and wrapper/node.go:28 import api/nodewright/v1alpha1 unaliased, so writer and reader agree on nodewright.nvidia.com/runtimeRequiredCordon, matching the docs and the e2e fixture. (This is the same fact the existing review reaches from the other direction: the legacy-package constant is dead, never written to any node.)
  • The license-header change in six generated YAML files is convergence, not drift. scripts/license-header.tmpl emits exactly one blank comment line between the SPDX block and "Licensed under", and those six files are the controller-gen-regenerated ones catching up to it. No action needed.

Also checked: the forward-only conversion assignment is complete, since no reverse NodeWright -> Skyhook converter exists.

// preserve the persistent cordon. Note that any runtime-required Skyhooks that run initially are free to
// add and remove the interrupt cordon because the persistent cordon is only applied when the runtime-required
// taint is removed.
_, ok := annotations[v1alpha1.RuntimeRequiredCordonAnnotation]

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.

Highest-severity finding here, and not covered elsewhere on this PR. This line does two things beyond what it looks like, and both outlive the NodeWright.

1. The cordon is never reversed by the finalizer. Finalizer cleanup is node.Uncordon() followed by node.CleanupSCRMetadata() (skyhook_controller.go:1924-1925). CleanupSCRMetadata only deletes keys matching prefix nodewright.nvidia.com/ and suffix _<nodewrightName>; runtimeRequiredCordon carries no name suffix, so it survives. And Uncordon no longer clears Spec.Unschedulable, because hasSkyhookCordon now returns true on the strength of that very annotation.

So deleting the NodeWright leaves the node unschedulable with an orphaned annotation. helm uninstall is worse: its cleanup-skyhooks-job pre-delete hook runs kubectl delete nodewrights --all, after which nothing exists that could clear it. This is the repo invariant that node-level mutations made outside the owner-reference tree must be finalizer-reversible.

2. The annotation becomes global cordon state. This is the part I have not seen raised yet. hasSkyhookCordon is consulted by every NodeWright's Uncordon, not just runtime-required ones. Once this key is on a node, any later, unrelated NodeWright that cordons it for an interrupt will remove its own cordon_<name> annotation and then silently fail to uncordon. An SRE debugging that node enumerates cordon_*, finds none, and has no explanation for why it stays unschedulable.

That is also why docs/interrupt_flow.md:88 is now false: "Only the cordon_* annotation family participates in shared cordon ownership." It no longer is. Lines 94-96 of that page tell users to run kubectl uncordon to recover, which clears spec.unschedulable but leaves the annotation, so the next interrupt cordon sticks. docs/uninstall.md:88 has the same problem (see the review body).

If the persistent cordon is meant to survive CR deletion, that is defensible, but the annotation likely needs to be scoped so it only gates the NodeWrights that asked for it, and both doc pages need updating.

if new_node.Annotations == nil {
new_node.Annotations = make(map[string]string)
}
new_node.Annotations[v1alpha1.RuntimeRequiredCordonAnnotation] = annotationTrueValue

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.

This is the only place the persistent cordon is ever written, which has a consequence worth stating alongside the wrapper/node.go finding.

The write sits inside the if updated branch, gated on the runtime-required taint existing. So the annotation is applied exactly once, at taint-removal time, and never re-converged. Combined with nothing removing it (the finalizer cannot, and neither reset command touches it), its lifecycle is write-once-never-delete from the operator's side.

The consequence: if someone runs kubectl uncordon and clears spec.unschedulable without deleting the annotation, the operator never re-cordons the node, yet the stale annotation keeps blocking Uncordon for every other NodeWright. The node lands in a state that is neither cordoned nor releasable, and the documented recovery patch (which clears both together) is the only thing that produces a clean result. Worth deciding whether reconcile should re-converge on the annotation rather than only writing it at the taint-removal edge.

To be clear, the patch itself is correct and idempotent: taints.RemoveTaint deep-copies the node so client.MergeFrom(node) is computed against an unmutated base, the taint and the cordon land atomically (closing the window where the node could be briefly schedulable), and the taint-exists guard makes a repeat reconcile a no-op. The single-patch design and the comment explaining it are good.

kind: NodeWright
name: runtime-required-cordon-after
format: yaml
steps:

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.

This test has no finally block, and it mutates node-level state that chainsaw does not own.

It taints kind-worker with skyhook.nvidia.com=runtime-required:NoSchedule in setup and relies on the operator plus the Phase 5 patch to undo the taint and the cordon. If any phase times out, none of that runs. The sibling test k8s-tests/chainsaw/nodewright/runtime-required/chainsaw-test.yaml ends with finally: ../nodes_remove_taint.sh all skyhook.nvidia.com=runtime-required:NoSchedule ... for exactly this reason.

Both tests carry pool: interrupt, and that pool runs --parallel 1 against a shared kind cluster, so leftovers are inherited by everything after. shared-cordon-ownership runs in the same pool and asserts the node ends up schedulable; it would fail, because the leftover runtimeRequiredCordon annotation makes Uncordon a no-op. Its own cordon_* check would pass first, so the failure output points at the wrong thing entirely.

One real flake here becomes a cascade of misleading failures elsewhere. A finally that removes the taint, the cordon and the annotation would contain it.

Expect(runtimeRequiredCordonAfterEnabled([]SkyhookNodes{sh1, sh2})).To(BeTrue())
})
Context("HandleRuntimeRequired with runtimeRequiredCordonAfter", func() {
runtimeRequiredTaintKey := "skyhook.nvidia.com"

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.

Trivial and test-only, but worth a line since it silently weakens the specs below it.

This hardcodes the runtime-required taint key as "skyhook.nvidia.com" rather than deriving it from opts.GetRuntimeRequiredTaint().Key, which the surrounding specs already use to build the taint they apply.

The literal matches today so the specs pass. But the assertions are of the form Expect(t.Key).ToNot(Equal(runtimeRequiredTaintKey)), so if the configured key ever changes they become vacuously true and stop catching a regression where the taint is not removed at all.

runtimeRequiredTaintKey := opts.GetRuntimeRequiredTaint().Key keeps the assertion coupled to the thing under test.

@natherz97

natherz97 commented Aug 10, 2026

Copy link
Copy Markdown
Author

@ayuskauskas thanks for the review, working through the edits but I wanted to clarify if I should make changes to the kubectl nodewright reset command. I'm not sure if it's straightforward for us to determine if the runtimeRequiredCordon annotation and cordon can be removed when we execute a reset. We're not tracking which runtime-required NodeWright resulted in the node receiving the persistent cordon and it's possible that multiple runtime-required NodeWrights all set runtimeRequiredCordonAfter to true. Additionally, we can't reliably look this up since we preserve the annotation and cordon even the NodeWrights which set runtimeRequiredCordonAfter to true are deleted. I was planning on not allowing the reset command to touch the runtimeRequiredCordon annotation nor cordon state of a node but let me know if you disagree. If we're on the same page, I'll proceed with a documentation update.

@lockwobr answering your questions

  1. Is stranding the cordon after NodeWright deletion intentional? Yes, this is intentional. I referenced the behavior specified in this comment, "It also survives SCR completion, SCR deletion, and the node no longer being targeted" [FEA]: Allow NodeWright to add a persistent node cordon #284 (comment).
  2. Should the cordon be re-converged level-triggered? Good callout, I'm wondering if we should actually do the opposite and remove the annotation if the cordon was manually removed. An operator would have to be intentional about removing the cordon. If they missed removing the annotation, I think a better user-experience would be the annotation being removed but not having the cordon re-applied What do you think?
  3. Should the webhook reject runtimeRequiredCordonAfter: true with runtimeRequired: false? If runtimeRequired + autoTaintNewNodes do not have this validation currently I will plan to hold off implementing this for runtimeRequired + runtimeRequiredCordonAfter together but let know if you like this added.
  4. Should kubectl nodewright node status surface this cordon? Good catch, let me add this.
  5. The cordon is never reversed by the finalizer. This is calling out the same concern from the first question that NodeWright deletion won't clear the cordon right? Orphaning the cordon and annotation is the expected behavior but let me know if this is something we should revisit.
  6. The annotation becomes global cordon state. This also is the expected behavior that all Skyhooks have to consult the global cordon state whether they're runtime required or not right? Skyhooks which aren't runtime-required still have to maintain the cordon if they're running an interrupt so I don't think there's a way around this without moving to a taint that could be managed independently but that's not possible if we need the node cordon.

I'll push a revision if this sounds reasonable to you guys.

@github-actions

Copy link
Copy Markdown

@natherz97 this PR now has merge conflicts with main. Please rebase to resolve them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/ci CI workflows, GitHub Actions, and repo tooling component/operator Skyhook operator (controller-manager) component/tests End-to-end / chainsaw test suites (k8s-tests) doc Documentation change (PR path label; doc issues use the Documentation type) needs-rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA]: Allow NodeWright to add a persistent node cordon

3 participants