Repository navigation
fix(activate): fail when an /etc file could not be written - #551
ndam-hexagon wants to merge 11 commits into
Conversation
91055de to
2bda917
Compare
jfroche
left a comment
There was a problem hiding this comment.
I think we should also handle the failed configured files/dir and retain their previous state. Otherwise they are treated as removed from the configuration, removed from the persisted state and will be considered unmanaged on the next activation.
| final_state.write_to_file(state_file)?; | ||
|
|
||
| if let Err(e) = tmp_result { | ||
| return Err(e.into()); |
There was a problem hiding this comment.
We could also handle tmp failure the same way.
I did something similar in #378 maintaining a list of error.
Maybe we could merge this feature here ?
There was a problem hiding this comment.
Held off on this one. With the early bail above, there is no longer a list of etc errors running alongside the tmp error to merge; the only accumulation left lives inside the etc walk itself. Tmp handling is untouched here.
A shared error list across etc/tmp/services still seems like the right shape, but #378 already has it, and growing a second mechanism here would just make that merge harder. Happy to rebase on top of #378 once it lands.
…failed An activation in which systemd refused a job, userborn.service failed or the services phase ended early (a job timeout, a D-Bus error) logged the error, wrote the state and exited 0. A caller judging the switch, a person or an unattended upgrade, then took a half-applied profile for an applied one. The control flow is unchanged: every unit is still attempted and the state file still records what was applied. Only the result changes: - exit 1 (ActivationFailure::Incomplete) when part of the profile wasn't applied, naming each part: a job systemd refused (the TODO in for_each_unit), including for sysinit-reactivation.target; a failed userborn.service (a oneshot's job finishes whether or not it succeeded, so its result is checked); a services phase that returned a partial result; - exit 4 (ActivationFailure::UnitsFailed) when everything was applied and managed units are in the failed state afterwards, naming them, as NixOS' switch-to-configuration does, so a caller can tell the two apart and judge the second against what was failing before. Stopping a unit systemd no longer has loaded (one that had exited, whose file the new profile dropped) fails with NoSuchUnit. That isn't a refusal, there is nothing to stop, so it is now logged at debug instead of as an error. /etc is left as it is: numtide#551 covers it.
|
We hit the same bug running system-manager unattended (an upgrade loop that switches, checks health and rolls back), and wrote a fix for Your tests run
I'm happy to push it to your branch or open it as a follow-up once this lands, whichever you prefer. One question about the last case. We made an unmanaged file in the way exit 1 rather than warn. Our reasoning was that the entry wasn't applied, and a copy entry in the same spot was already an error at the file level. #378's VM test expects a failure there too. I can see the case for keeping it a warning: a distribution image that ships a file at a path a module manages without |
The format string reads as a source/target pair but only ever received the target and the error, so the target landed in the source slot: "Can't link/copy /etc/nix/nix.conf to : Is a directory (os error 21)". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A directory at a managed path was reported as a per-file warning and skipped, because fs::remove_file is EISDIR on a directory and backup_existing_file's rename puts an unrestorable directory in the backup slot. Activation then reported success with /etc untouched, and every later run took the same branch, so it never healed. Clear the directory before choosing how to replace the target, but only where we already intend to replace it, so the "never clobber an unmanaged path" guard keeps its current meaning. An empty directory is dropped and the target is created normally; a non-empty one is an error rather than a recursive delete under /etc driven by a state file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fs::rename silently clobbers its destination, so a target that is not in the state file but already has a .system-manager-backup next to it, as happens after the state is lost, destroyed the original the backup was holding. Deactivation then "restored" our own file over the top. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Result was dropped on the floor, so a parent that cannot be created (notably a dangling symlink, where create_dir_all returns EEXIST) only surfaced later as a confusing ENOENT from the symlink or copy that followed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A managed target that is missing from disk was re-created and filed unconditionally under the plain-files set, even when the previous state had it as backed up. Deactivation restores only from the backed-up set, so the original was orphaned in its .system-manager-backup forever. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A per-file failure under /etc was logged as a warning and then dropped, so activation reported success while the file it could not write stayed as it was, with nothing on the machine able to tell. That is the whole "success proves nothing" surface: the switch exits 0 and the operator has no signal at all. Collect the per-file failures during the walk and fail at the end, keeping the warnings and keeping the rest of the walk going, so one bad file still does not stop the others from landing. Deliberate skips stay non-fatal, which is the distinction that makes this safe: an unmanaged path without replaceExisting, and the distro units under /etc/systemd/system that we knowingly cannot own, are choices rather than failures and go on warning with a zero exit. Only an attempted write that failed counts. Aborting the walk outright, when the entry list or the static env cannot be read, now fails too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exercises create_etc_files against a temporary /etc, so the behaviour is checked without the cgroups and auto-allocate-uids features the container tests need. Four of these fail without the preceding fixes. The two skip cases pass either way on purpose: they pin the lenient half of the contract, that a path we choose not to clobber is not a failure, which is what keeps the new non-zero exit from firing on the distro units under /etc/systemd/system that we knowingly cannot own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A target we failed to write was absent from the new state, so it fell into the deletion set: we removed the file we had just failed to replace, and forgot it was ever managed, leaving the next activation to treat it as unmanaged. Carry it over from the old state instead. The failure list now holds the same absolute paths as the state, which the lookup needs, and which also makes the warnings point at a real file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collecting per-file failures instead of aborting the walk meant activation carried on to restart services on top of configuration we knew we had failed to write. Persist the state and fail before that point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
166ee72 to
03f5fdb
Compare
That is actually an oversight on my end. On main Feel free to push the container test onto my branch. In the meantime, I dropped the no-replace entry from I added a #582 LGTM |
|
Thanks! I don't have push access to your fork, so the container test is on mine: c395fee, branch
The last case encodes the warning as it is today: the CHANGELOG entry and the
I ran it on your branch plus this commit on GitHub's hosted runners. It passes on Debian 13 and Ubuntu 24.04 and 26.04, and the rest of the container suite was green there too (served from the numtide cache for 03f5fdb). Ubuntu 22.04 I couldn't check: on those runners a 22.04 container never finishes booting (logind and networkd time out), for every test, whatever the branch. Your nixbot run will cover it. On how the two PRs fit together: once #551 and #582 are both in, an |
|
Thank you both @ndam-hexagon and @jeremy for all your work on this. It really proves error handling needs some love. I would merge this one before #582 After that, and based on @picnoir recommendation in #378 (review) , I will try to reduce the possibility to face these /etc issues using pre-flight checks. home-manager is also doing this. I would also like to prevent write error using the idea behind atomicSymlink from nixpkgs. For the moment we remove the file then try to symlink, we should write to a temporary target and rename, it would make the operation more atomic. Ideal would be what @r-vdp described in #212 but that one is much larger. |
Co-authored-by: Jean-François Roche <jfroche@pyxel.be>
Co-authored-by: Jean-François Roche <jfroche@pyxel.be>
system-manager switchexits 0 even when it failed to write a file under/etc. The failure is only a warning, so the generation is marked active while/etcstill has the old content.Now those failures are collected during the walk and reported at the end, so one bad file still doesn't stop the others from landing. Files we deliberately leave alone stay non-fatal: an unmanaged path without
replaceExisting, and the distro units under/etc/systemd/systemwe can't own, keep warning with a zero exit. Only a failed write counts.Five bugs found on the way, one commit each:
remove_filegivesEISDIR, and renaming it into the backup slot leaves something that can't be restored. Now an empty directory is removed and the file created normally; a non-empty one is an error, not a recursive delete under/etc.backup_existing_fileusedfs::rename, which overwrites. If a.system-manager-backupwas already there (e.g. after losing the state file), the real original was destroyed and deactivation put our own file back instead.create_dir_allresult was ignored, so a parent that can't be created showed up later as a confusingENOENT.Can't link/copy /etc/nix/nix.conf to : Is a directory.The tests run
create_etc_filesagainst a temp/etc, so they don't need the cgroups and auto-allocate-uids features the container tests need. Four fail without the fixes. The two skip cases pass either way on purpose, to pin down what must not fail activation.No existing issue covers this as far as I could find; happy to open one if you want the link.
🤖 Generated with Claude Code