Repository navigation
Conversation
| if err: | ||
| return err | ||
| return _wait_for_kubelet_hugepages(k8s_core_v1, node_name, required_mib) | ||
| return None |
|
Rebased onto main (103 commits behind; one test conflict from the port-block removal) and extended with the machine-config mode, on branch What changed: Lab (Talos v1.12.7, 4 workers, operator
One gap seen on the way, not fixed here: the operator's 🤖 Generated with Claude Code |
…d huge pages On Kubernetes the storage node reserves huge pages while it is added: the init Job POSTs apply_config back to the node agent, which writes nr_hugepages per NUMA node and restarts the kubelet. Talos cannot take any of that -- the host is immutable and has no shell -- and the only thing that told the Job so was a sniff of /proc/version for the word "talos". Nothing made the behaviour configurable, and nothing checked that the pool the machine config provides is big enough for the SPDK pod that is about to request it: a short pool left the pod Pending on "Insufficient hugepages-2Mi" with no message naming the number to provision. Talos is now stated the way OpenShift is: TALOS_CLUSTER on the node agent, set by the operator from the deployment's environment (as OPENSHIFT_CLUSTER is). A stated Talos node * compares Hugetlb from /proc/meminfo against the pod's hugepages-2Mi request before creating anything, and fails the add with both numbers and the vm.nr_hugepages value to set when the pool is short; * skips the nr_hugepages write and kubelet restart in the init Job through one MANAGE_HUGEPAGES variable, which the kernel-version sniff now only feeds as a fallback for a deployment that did not state Talos; * skips the CPU-topology / core-isolation Jobs, which nsenter into the host (kubelet config, grubby), with a warning instead of a failing Job. Managed hosts are untouched: without the flag no pool check is made, since the pages are about to be reserved by the Job itself. docs/talos.md says how the flag reaches the node and how to size the pool. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A stated Talos node (TALOS_CLUSTER) no longer just checks the pool the machine config happens to provide; it reserves the pages itself, the way the init Job's apply_config does on other hosts, through the only channel Talos offers: a Job running talosctl against the node's own apid. * _provision_talos_hugepages: pool already covering the SPDK pod's request -> nothing applied (no kubelet restart on a plain node restart). Short -> render storage_talos_hugepages_job.yaml.j2: an init container runs `talosctl patch machineconfig --mode=no-reboot` with a strategic-merge patch putting the per-NUMA counts into machine.sysfs (devices.system.node.nodeN.hugepages.hugepages-2048kB.nr_hugepages, applied live, merged into whatever else is there), the main container runs `talosctl service kubelet restart` so the node's hugepages-2Mi capacity follows. talosctl's image is scratch + binary, hence two containers and no shell. Credentials: the Secret Talos issues for a talos.dev/v1alpha1 ServiceAccount in our namespace (Talos API access from Kubernetes), mounted where talosctl looks by default. * The add then waits until both the host (Hugetlb) and the kubelet (status.capacity hugepages-2Mi) cover the request and fails with the numbers if either does not. A missing credentials Secret fails fast with the setup steps instead of a Job stuck in ContainerCreating. * The sizing is apply_config's, factored into _hugepages_per_numa and shared, so a Talos host gets the same pool a sysfs-written one does. * Knobs: TALOSCTL_IMAGE (pin to the cluster's Talos minor, mirrorable) and TALOS_API_SECRET; defaults in constants.py. docs/talos.md: the control-plane prerequisite (KubeTalosAPIAccessConfig / machine.features.kubernetesTalosAPIAccess with os:admin for the namespace, the talos.dev ServiceAccount) and the knobs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…patch Verified on the lab Talos v1.12.7 cluster: its machine config is multi-document and a JSON 6902 patch is refused there, so the merge patch is the only format that works everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t between them First full-flow test on the lab Talos cluster (v1.12.7, four workers, 2026-10-16): two of the four node adds failed with "the kubelet still reports 0 MiB of hugepages-2Mi capacity". The one Job patched machine.sysfs in its init container and restarted the kubelet in its main container, but Talos writes the kernel parameter asynchronously after accepting the patch -- so on worker-1 and worker-3 the kubelet came back before the pages existed and advertised zero. The pool was there seconds later; only a second kubelet restart fixed the capacity. On worker-2 and worker-4 the write happened to win the race. The flow is now two Jobs with the agent waiting in between: 1. host pool short -> `talosctl patch machineconfig` Job, then wait until /proc/meminfo shows the pool; 2. kubelet capacity short -> `talosctl service kubelet restart` Job, then wait until the node advertises it. Each step is skipped when its half already holds, so a node whose kubelet merely lags the host (the retry case above) gets only the restart, and a plain restart with the pool in place touches nothing. The template renders one container for the step it is given; talosctl's image has no shell to sequence two commands in. What talosctl printed is copied into the agent log before a completed Job is deleted, since the Job was the only record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…U topology through the Talos API Detection: the kubelet publishes the host's os-release in the Node object (status.nodeInfo.osImage, "Talos (v1.12.7)"), so the agent reads it there instead of being told. TALOS_CLUSTER becomes an override (true/false) for a Talos derivative whose osImage says something else, or to force the managed path; a deployment that never sets it -- an older operator, hand-written CRs -- now takes the Talos path on its own. Kubelet CPU topology: the vanilla CPU-topology Job edits the kubelet's config file (cpuManagerPolicy static, reservedSystemCPUs, topologyManager single-numa-node / pod) and restarts the kubelet, which Talos has no file for. The same keys go into machine.kubelet.extraConfig through a third talosctl Job; Talos applies that without a reboot and restarts the kubelet itself, and the add waits for the node to be Ready again. The step reads the kubelet's live configuration first (nodes/<name>/proxy/configz) and patches only when the settings are missing: Talos restarts the kubelet on every machine-config change, so an unconditional patch would restart it on every SPDK restart. Without nodes/proxy on the role the step is skipped with a warning for the same reason. Core isolation (kernel arguments) stays out: on Talos those take an upgrade/reboot cycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… of it
The Kubernetes client's preloaded 'str' result for the node proxy call is
str(bytes) rendered as a Python dict repr ("{'kubeletconfig': {...}}"),
not JSON, so the topology check always answered "cannot verify" and the
step was skipped (lab cluster, worker-4, 2026-10-21). Ask for the raw
response and decode its body.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pod lands on its own cores The static CPU manager knows two sets: reservedSystemCPUs, never handed out exclusively, and all the rest, from which it picks the exclusive CPUs of every integer-CPU Guaranteed container -- any of them. There is no way to name the CPUs a pod must get, so with the fixed '0,1' the SPDK pod ran on whatever cores the kubelet chose, not the ones sn configure laid out (and isolcpus isolates), and the restart path chased them. The reserved set is now derived on the node as the complement of the node config's isolated_cores across every socket; RESERVED_SYSTEM_CPUS, which the operator only sets when the StorageCluster states it, overrides. Normal pods are unaffected: the kubelet runs them on the reserved CPUs too -- unless strict-cpu-reservation is on, which is refused with a message. The proxy container asks for 500m (requests equal to limits): the pod stays Guaranteed, the SPDK container keeps its exclusive cores, and the proxy no longer takes an exclusive core of its own -- the one that the full-pcpus-only option rejected as odd on chess (R25.10 #940) and that would have had to come out of a pool now sized to the SPDK cores. The vanilla CPU-topology Job is idempotent on the kubelet config's content, not a marker file, since the reserved set changes with the core layout. On Talos, where the kubelet refuses to start over a checkpoint written under another policy and there is no shell to remove it from, a privileged hostPath Job removes cpu_manager_state right before the machine-config patch. The reserved value is written quoted everywhere so a single CPU id is not read as a number. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ot the SPDK layout's complement Reserving everything but the SPDK cores did put the SPDK pod on exactly its planned cores (lab worker-4, 2026-10-21: isolated 17,19,21,23 -> reservedSystemCPUs 0-16,18,20,22, patched and confirmed) -- but the kubelet also sizes the node's allocatable CPU as capacity minus the reserved list, so allocatable went from 22 to 4 and the SPDK pod (its cores plus the proxy) and every other pod's request no longer fit: it sat Pending on "Insufficient cpu". The pattern only works where the non-reserved pool is most of the node, which our SPDK share is not. reservedSystemCPUs is RESERVED_SYSTEM_CPUS again (the StorageCluster's reservedSystemCPU, default 0,1); which cores the Guaranteed SPDK pod gets is the kubelet's pick from the non-reserved set, and the restart path follows it as before. Kept from the previous commit: the proxy at 500m (no second exclusive core), the content-based CPU-topology Job (a changed reserved set is applied), the strict-cpu-reservation refusal, and the Talos checkpoint removal before the patch. The docs state the allocatable coupling so the set gets sized with it in mind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stated managed Until now a Talos node reached for the Talos API whenever its huge-page pool or kubelet CPU topology fell short, which made an os:admin Secret in the namespace a de-facto requirement, and told an operator who had chosen to prepare the machine config themselves to go and create one when they had merely under-sized a pool. TALOS_MACHINE_CONFIG now picks the mode, from the StorageCluster's storageNodes.talosMachineConfig through the operator: * preprovisioned (the default, and what an unknown value falls back to): the node checks the pool, the kubelet's advertised capacity and the kubelet's live configuration as before, applies nothing, and fails the add with the machine-config patch to apply, sized for that worker and in the form `talosctl patch mc` takes. A pool the host has but the kubelet does not advertise (the boot-time race) is reported as the kubelet restart it needs. No Talos API credentials exist anywhere. * managed: the talosctl Jobs as before. A worker that already carries everything is left alone in either mode. docs/talos.md describes both, with the pre-provisioning recipe and its two pitfalls (the kubelet's start-time capacity read, the CPU-manager checkpoint) first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2f0a81e to
1ec0666
Compare
…, in its message CodeQL flagged the node-add refusal as logging a secret in clear text: the pre-check's message carried the Secret's *name*, but that name comes out of TALOS_API_SECRET through a function and variables named "secret", and the scanner's rule is name-based. The message now names the setting and its default instead of the configured value, the helper is _talos_api_access_missing, and the warning on an unreadable Secret no longer interpolates the name either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Why
On Kubernetes the storage node reserves huge pages while it is added: the init Job POSTs
apply_configback to the node agent, which writesnr_hugepagesper NUMA node and restarts the kubelet. Talos cannot take any of that (immutable host, no shell), and the only thing that told the Job so was a sniff of/proc/versionfor the wordtalos, after which the node simply skipped. Nothing reserved the pages, and a pre-provisioned pool that was short left the SPDK pod Pending onInsufficient hugepages-2Miwith no message naming the number.What
Talos is stated the way OpenShift is:
TALOS_CLUSTERon the node agent, set by the operator from the deployment's environment (asOPENSHIFT_CLUSTERis). A stated Talos node reserves its huge pages through the Talos API:_provision_talos_hugepageschecks the host's pool against the pod'shugepages-2Mirequest. A pool that already covers it is left alone (no kubelet restart on a plain node restart).storage_talos_hugepages_job.yaml.j2: a Job on the node running the officialtalosctlimage (scratch + binary, so one container per command). An init container runstalosctl patch machineconfig --mode=no-rebootwith a strategic-merge patch putting the per-NUMA counts intomachine.sysfs(devices.system.node.node<N>.hugepages.hugepages-2048kB.nr_hugepages, applied live, merges into whatever else is inmachine.sysfs); the main container runstalosctl service kubelet restartso the node's capacity follows. Credentials are the Secret Talos issues for atalos.dev/v1alpha1 ServiceAccountin our namespace, mounted wheretalosctllooks by default.Hugetlb) and the kubelet (status.capacity["hugepages-2Mi"]) cover the request, and fails with the numbers if either does not. A missing credentials Secret fails fast with the setup steps instead of a Job stuck in ContainerCreating.apply_confighas always used, factored into_hugepages_per_numaand shared by both paths, so a Talos host ends up with the same pool as a sysfs-written one.nr_hugepageswrite and kubelet restart through oneMANAGE_HUGEPAGESvariable (the kernel sniff now only feeds it, as a fallback), and the CPU-topology / core-isolation Jobs, whichnsenterinto the host, are skipped with a warning.Managed hosts are untouched: without the flag nothing Talos-related runs.
Knobs:
TALOSCTL_IMAGE(defaultghcr.io/siderolabs/talosctl:v1.14.3, pin to the cluster's minor / mirror for air-gapped sites) andTALOS_API_SECRET(defaulttalos-api-access-talos-secrets, thetalosctl inject serviceaccountnaming).docs/talos.mdhas the cluster-side prerequisite (enableKubeTalosAPIAccessConfig/machine.features.kubernetesTalosAPIAccesson the control plane withos:adminfor the simplyblock namespace, create thetalos.devServiceAccount) and the knobs.Detection and the kubelet CPU topology (added 2026-10-21)
status.nodeInfo.osImageof its Node (Talos (v1.12.7)), the same source discovery derivesenvironment: Talosfrom.TALOS_CLUSTER=true|falseis now only an override (a derivative with a different osImage, or forcing the managed path); an older operator or hand-written CRs take the Talos path on their own. The kernel-string sniff inside the init Job stays only as the fallback for that script.cpuManagerPolicy: static,reservedSystemCPUs,topologyManagerPolicy: single-numa-node,topologyManagerScope: pod) are merged intomachine.kubelet.extraConfigby a thirdtalosctlJob. Talos applies it without a reboot and restarts the kubelet itself; the add waits for the node to be Ready again and checks the result. It runs under the usual switches (CPU_TOPOLOGY_ENABLEDand notSKIP_KUBELET_CONFIGURATION).nodes/<name>/proxy/configz; a kubelet that already runs with the settings is left alone, since Talos restarts the kubelet on every machine-config change and a restart per SPDK restart would be a regression. Withoutnodes/proxy geton the role (operator Main lvol sync delete #734 adds it) the step is skipped with a warning for the same reason....-42d2689d, operator025137ba). The kubelet there already rancpuManagerPolicy: staticviamachine.kubelet.extraArgsbut withoutreservedSystemCPUs, so configz readstatic / None; the restart createdsnode-talos-kubelet-config-worker-4(10:36:43), the merge patch landed inmachine.kubelet.extraConfig, Talos restarted the kubelet (SIGTERM/started at 10:36:45), the node stayed Ready, configz readstatic / 0,1 / single-numa-node / podand the SPDK pod started at 10:37:04. A second restart loggedalready runs with the CPU topology settings; nothing to apply, created no Job and did not restart the kubelet. One fix came out of the first attempt: the client's preloadedstrfor the proxy call is a Python dict repr, not JSON (42d2689 reads the raw body).isolcpusetc.) is not applied on Talos: kernel arguments live inmachine.install.extraKernelArgsand need an upgrade/reboot cycle. Documented.Operator side (separate PR, simplyblock-operator)
TALOS_CLUSTERfromspec.environment: Talos:Talos *boolonStorageNodesSpecnext toUbuntuHost,workload.Talos = ptr.To(true)in theKubernetesEnvironmentTaloscase ofexpansion.go,{Name: "TALOS_CLUSTER", Value: ptr.StringOrDefault(wl.Talos, "false")}instorage_node_workload.go.TALOSCTL_IMAGEthrough (image mirroring).talos.dev/v1alpha1 ServiceAccountin the namespace when the environment is Talos, and grantsimplyblock-storage-node-sagetonsecretsso the agent's fast-fail check works (today it has no Secret RBAC; the check skips on 403 and the Job reports instead).Tests
tests/unit/test_talos_hugepages.py(26 tests): the sizing helpers (_hugepages_per_numaequals theapply_configfigures, the sysfs patch shape, Kubernetes quantity parsing, Job-name truncation), the Job template (patch then restart, no shell, Secret mount,NODE_IPfromstatus.hostIP), and the handler underTALOS_CLUSTER=true: short pool → Talos Job with the per-NUMA patch, kubelet restart, then the SPDK pod; sufficient pool → nothing applied; missing Secret → fast fail naming the setup; unreadable Secret (403) → proceeds; Job failure → reported with the Job name; pool still short / kubelet not following → add fails with the numbers; image and Secret from env; core-isolation Job skipped; no flag → nothing Talos-related. Plus the init-script tests (flag renders, one gating variable, dash parse and a stated-Talos run making nocurl/nsentercall — the dash cases run in CI).Tested on the lab Talos cluster (2026-10-16)
Talos v1.12.7, Kubernetes v1.35.4, one control plane + four workers (24 vCPU / 24 GiB, 4x QEMU NVMe on vfio-pci, single NUMA). Talos API access enabled on the control plane for
os:adminin namespacesimplyblock;talos.dev/v1alpha1 ServiceAccountcreated, Secret issued within seconds.Mechanism (morning): the rendered Job on worker-1 took the pool 6200 → 6300 pages and the kubelet capacity 12400Mi → 12600Mi in ~30 s, node never left Ready.
talosctl patch mc --dry-runconfirmed the dotted sysfs key; a JSON 6902 patch is refused on this multi-document config, the merge patch is accepted.Full node-add flow (afternoon, with operator #734 emitting
TALOS_CLUSTER=true, all worker pools cleared to 0 first): discovery detectedenvironment: Talosand the NVMe PCI addresses on its own; all four nodes were added through the Talos Jobs (3057 pages each,hugepages-2Mi0 → 6114Mi), SPDK pods request 5390Mi, clusterOnline/activeat 13:01 UTC.Finding, fixed in the last commit: two of the four adds failed their first attempt with "the kubelet still reports 0 MiB". The single Job patched
machine.sysfsin its init container and restarted the kubelet in its main container, but Talos writes the kernel parameter asynchronously, so on worker-1 and worker-3 the kubelet came back before the pages existed and advertised zero; a second kubelet restart fixed it. The flow is now two Jobs with the agent waiting for the host pool in between, each step skipped when its half already holds, and talosctl's output copied into the agent log. Two-Job flow verified (13:25 UTC, agent image...-36ef2ea9): worker-4 shut down, its pool dropped to 0 and kubelet restarted (capacity 0), thensbctl sn restart: patch Job 13:25:45 → host pool 3057 pages → kubelet Job 13:25:48 → capacity 6114Mi by 13:25:59 → SPDK pod 13:26:09, no manual help. A second restart with the pool in place loggedalready covers ... nothing to applyand created no Job, so a plain restart never restarts the kubelet.Also seen:
images.talosctlon the deployment config was rejected byImageSpec's registry allow-list (fixed on the operator branch withExternalImageSpec), so this run used the agent's defaulttalosctl:v1.14.3against Talos v1.12.7 — the version skew worked.Machine-config mode: pre-provisioned by default, managed on request (2026-11-02)
Applying the huge-page pool and the kubelet CPU topology through the Talos API needs
os:admincredentials in the simplyblock namespace, which not every site wants to hand out. Both steps now run in one of two modes, chosen byTALOS_MACHINE_CONFIGon the node agent (the operator sets it fromstorageNodes.talosMachineConfig, which the ClusterDeploymentConfig states asspec.cluster.talosMachineConfig; operator #734):preprovisioned(default): the machine configs already carry the pool and the kubelet settings, written by whoever provisions the cluster. The agent only verifies them (host pool viaHugetlb, the kubelet'shugepages-2Micapacity, the kubelet's liveconfigz) and applies nothing. A node that falls short is refused, and the refusal carries the exact patch to apply withtalosctl -n <node ip> patch mc -p @patch.yaml: the per-NUMAmachine.sysfscounts, or themachine.kubelet.extraConfigblock. A pool that the host has but the kubelet does not advertise yet gets a "restart the kubelet" hint instead of a patch. No Talos API credentials are needed.managed: the behaviour this PR had so far: the talosctl Jobs apply the patches, which needs thetalos.dev/v1alpha1 ServiceAccountSecret.An unknown value is treated as pre-provisioned with a warning.
docs/talos.mdnow describes both modes, the pre-provisioning recipe (machine.sysfs+machine.kubelet.extraConfig) and the two pitfalls seen on the lab: the kubelet reads its huge-page capacity at start, so a pool written after boot needstalosctl service kubelet restart; and a kubelet that ran under another CPU-manager policy needs/var/lib/kubelet/cpu_manager_stateremoved, which the managed mode does by itself.Tests:
TestSpdkProcessStartOnPreprovisionedTalos(8 tests) next to the managed-mode class: the sysfs refusal with the patch, the patch equal to the managed-mode one, the kubelet-lag hint, a sufficient node proceeding without credentials, an unconfigured node, the extraConfig refusal creating no Talos Job, an already configured kubelet proceeding, the unknown-mode fallback, and case-insensitive values.Lab run (2026-11-02, Talos v1.12.7, four workers, operator #734 at 50de1970)
cpuManagerPolicy: staticbut withoutreservedSystemCPUs/ topology manager, was refused with theextraConfigpatch in the agent log. Applying that patch verbatim withtalosctl patch mchad Talos restart the kubelet within about ten seconds, and the next node-add retry brought the node online without further help.Managedon the live StorageCluster: the DaemonSet re-rendered within seconds, worker-2 was refused naming the missing Secret, and after thetalos.devServiceAccount was created workers 2 and 3 went through the kubelet-state and kubelet-config Jobs and came online. ClusterOnline/active.Finding, not fixed here: the operator's
NodeAddFailingevent shows onlyNode add result: False(the task runner raisesTaskRetrywith that text,add_nodereturns a bool), so the refusal with the patch is only in the node agent's log. Propagating the agent's message into the task result would make the pre-provisioned mode usable without reading agent logs.Rebased on main 5543922.
🤖 Generated with Claude Code