Skip to content

Commit 42e7674

Browse files
committed
feat(module): add magic rollback options
1 parent 665d723 commit 42e7674

File tree

2 files changed

+138
-72
lines changed

2 files changed

+138
-72
lines changed

nix/module.nix

Lines changed: 134 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ in {
4747
prev;
4848
};
4949

50+
magic-rollback = {
51+
enable = lib.mkOption {
52+
type = types.bool;
53+
description = ''
54+
Enable `nixos-cli` magic rollback facilities for this system.
55+
56+
This option inserts a small binary into the NixOS system closure
57+
at `/path/to/closure/bin/activation-supervisor` that can be used
58+
by `nixos-cli` to control remote activation.
59+
60+
The activation supervisor runs the switch-to-configuration script
61+
on this system, and then watches for a signal from the deployer
62+
to confirm the activation; if no signal is given (i.e. if SSH
63+
or all internet access is disconnected), then an automatic rollback
64+
starts in order to regain connectivity again.
65+
66+
Enabling this option is required on the target systems that are
67+
deployed using `nixos-cli`. However, this does not require
68+
`nixos-cli` itself to be enabled on the target system.
69+
'';
70+
default = false;
71+
example = true;
72+
};
73+
74+
package = lib.mkOption {
75+
type = types.package;
76+
default = self.packages.${system}.activation-supervisor;
77+
description = "Package to use for the activation supervisor that controls magic rollback";
78+
};
79+
80+
# TODO: add an option to make rollback timeout configurable
81+
};
82+
5083
useActivationInterface = lib.mkOption {
5184
type = types.bool;
5285
default = false;
@@ -80,84 +113,114 @@ in {
80113
};
81114
};
82115

83-
config = lib.mkIf cfg.enable (lib.mkMerge [
84-
{
85-
environment.systemPackages = [cfg.package];
86-
87-
environment.etc."nixos-cli/config.toml".source =
88-
tomlFormat.generate "nixos-cli-config.toml" cfg.config;
89-
90-
# Hijack system builder commands to insert a `nixos-version.json` file at the root.
91-
system.systemBuilderCommands = let
92-
nixos-version-json = builtins.toJSON {
93-
nixosVersion = "${nixosCfg.distroName} ${nixosCfg.release} (${nixosCfg.codeName})";
94-
nixpkgsRevision = nixosCfg.revision;
95-
configurationRevision = "${builtins.toString config.system.configurationRevision}";
96-
description = cfg.generationTag;
97-
};
98-
in ''
99-
cat > "$out/nixos-version.json" << EOF
100-
${nixos-version-json}
101-
EOF
102-
'';
116+
config = lib.mkMerge [
117+
(lib.mkIf cfg.enable (
118+
lib.mkMerge [
119+
{
120+
environment.systemPackages = [cfg.package];
121+
122+
environment.etc."nixos-cli/config.toml".source =
123+
tomlFormat.generate "nixos-cli-config.toml" cfg.config;
124+
125+
# Hijack system builder commands to insert a `nixos-version.json` file at the root.
126+
system.systemBuilderCommands = let
127+
nixos-version-json = builtins.toJSON {
128+
nixosVersion = "${nixosCfg.distroName} ${nixosCfg.release} (${nixosCfg.codeName})";
129+
nixpkgsRevision = nixosCfg.revision;
130+
configurationRevision = "${builtins.toString config.system.configurationRevision}";
131+
description = cfg.generationTag;
132+
};
133+
in ''
134+
cat > "$out/nixos-version.json" << EOF
135+
${nixos-version-json}
136+
EOF
137+
'';
138+
139+
# FIXME: should this be configurable? Not all users would want to preserve
140+
# SSH_AUTH_SOCK, for example.
141+
security.sudo.extraConfig = ''
142+
# Preserve NIXOS_CONFIG and NIXOS_CLI_CONFIG in sudo invocations of
143+
# `nixos apply`. This is required in order to keep ownership across
144+
# automatic re-exec as root.
145+
Defaults env_keep += "NIXOS_CONFIG"
146+
Defaults env_keep += "NIXOS_GENERATION_TAG"
147+
Defaults env_keep += "NIXOS_CLI_CONFIG"
148+
Defaults env_keep += "NIXOS_CLI_DISABLE_STEPS"
149+
Defaults env_keep += "NIXOS_CLI_DEBUG_MODE"
150+
Defaults env_keep += "NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
151+
Defaults env_keep += "SSH_AUTH_SOCK"
152+
'';
153+
}
154+
(lib.mkIf cfg.prebuildOptionCache {
155+
# While there is already an `options.json` that exists in the
156+
# `config.system.build.manual.optionsJSON` attribute, this is
157+
# not as full-featured, because it does not contain NixOS options
158+
# that are not available in base `nixpkgs`. This does increase
159+
# eval time, but that's a fine tradeoff in this case since it
160+
# is able to be disabled.
161+
environment.etc."nixos-cli/options-cache.json" = {
162+
text = let
163+
optionList' = lib.optionAttrSetToDocList options;
164+
optionList = builtins.filter (v: v.visible && !v.internal) optionList';
165+
in
166+
builtins.toJSON optionList;
167+
};
168+
})
169+
(lib.mkIf cfg.useActivationInterface {
170+
# This looks confusing, but this only stops the switch-to-configuration-ng
171+
# program from being used. The system will still be switchable.
172+
system.switch.enable = lib.mkForce false;
173+
174+
# Use a subshell so we can source makeWrapper's setup hook without
175+
# affecting the rest of activatableSystemBuilderCommands.
176+
system.activatableSystemBuilderCommands = ''
177+
(
178+
source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
179+
180+
mkdir -p $out/bin
181+
182+
ln -sf ${lib.getExe cfg.package} $out/bin/switch-to-configuration
183+
184+
wrapProgram $out/bin/switch-to-configuration \
185+
--add-flags activate \
186+
--set NIXOS_CLI_ATTEMPTING_ACTIVATION 1 \
187+
--set OUT $out \
188+
--set TOPLEVEL ''${!toplevelVar} \
189+
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
190+
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
191+
--set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \
192+
--set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
193+
--set SYSTEMD ${config.systemd.package}
194+
)
195+
'';
196+
})
197+
]
198+
))
199+
(lib.mkIf cfg.magic-rollback.enable {
200+
assertions = [
201+
{
202+
assertion = cfg.useActivationInterface || config.system.switch.enable;
203+
message = ''
204+
`services.nixos-cli.magic-rollback` can only be used on switchable systems.
205+
206+
Either enable the `nixos-cli` activation interface or allow switching
207+
systems using `switch-to-configuration-ng`.
208+
'';
209+
}
210+
];
103211

104-
# FIXME: should this be configurable? Not all users would want to preserve
105-
# SSH_AUTH_SOCK, for example.
106-
security.sudo.extraConfig = ''
107-
# Preserve NIXOS_CONFIG and NIXOS_CLI_CONFIG in sudo invocations of
108-
# `nixos apply`. This is required in order to keep ownership across
109-
# automatic re-exec as root.
110-
Defaults env_keep += "NIXOS_CONFIG"
111-
Defaults env_keep += "NIXOS_GENERATION_TAG"
112-
Defaults env_keep += "NIXOS_CLI_CONFIG"
113-
Defaults env_keep += "NIXOS_CLI_DISABLE_STEPS"
114-
Defaults env_keep += "NIXOS_CLI_DEBUG_MODE"
115-
Defaults env_keep += "NIXOS_CLI_SUPPRESS_NO_SETTINGS_WARNING"
116-
Defaults env_keep += "SSH_AUTH_SOCK"
117-
'';
118-
}
119-
(lib.mkIf cfg.prebuildOptionCache {
120-
# While there is already an `options.json` that exists in the
121-
# `config.system.build.manual.optionsJSON` attribute, this is
122-
# not as full-featured, because it does not contain NixOS options
123-
# that are not available in base `nixpkgs`. This does increase
124-
# eval time, but that's a fine tradeoff in this case since it
125-
# is able to be disabled.
126-
environment.etc."nixos-cli/options-cache.json" = {
127-
text = let
128-
optionList' = lib.optionAttrSetToDocList options;
129-
optionList = builtins.filter (v: v.visible && !v.internal) optionList';
130-
in
131-
builtins.toJSON optionList;
132-
};
133-
})
134-
(lib.mkIf cfg.useActivationInterface {
135-
# This looks confusing, but this only stops the switch-to-configuration-ng
136-
# program from being used. The system will still be switchable.
137-
system.switch.enable = lib.mkForce false;
138-
139-
# Use a subshell so we can source makeWrapper's setup hook without
140-
# affecting the rest of activatableSystemBuilderCommands.
141-
system.activatableSystemBuilderCommands = ''
212+
system.activatableSystemBuilderCommands = lib.mkAfter ''
142213
(
143214
source ${pkgs.buildPackages.makeWrapper}/nix-support/setup-hook
144215
145-
mkdir $out/bin
216+
mkdir -p $out/bin
146217
147-
ln -sf ${lib.getExe cfg.package} $out/bin/switch-to-configuration
218+
ln -sf ${lib.getExe cfg.magic-rollback.package} $out/bin/activation-supervisor
148219
149-
wrapProgram $out/bin/switch-to-configuration \
150-
--add-flags activate \
151-
--set NIXOS_CLI_ATTEMPTING_ACTIVATION 1 \
152-
--set OUT $out \
153-
--set TOPLEVEL ''${!toplevelVar} \
154-
--set DISTRO_ID ${lib.escapeShellArg config.system.nixos.distroId} \
155-
--set INSTALL_BOOTLOADER ${lib.escapeShellArg config.system.build.installBootLoader} \
156-
--set PRE_SWITCH_CHECK ${lib.escapeShellArg config.system.preSwitchChecksScript} \
157-
--set LOCALE_ARCHIVE ${config.i18n.glibcLocales}/lib/locale/locale-archive \
158-
--set SYSTEMD ${config.systemd.package}
220+
wrapProgram $out/bin/activation-supervisor \
221+
--set TOPLEVEL ''${!toplevelVar}
159222
)
160223
'';
161224
})
162-
]);
225+
];
163226
}

nix/tests/example.test.nix

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
}:
66
pkgs.testers.runNixOSTest {
77
name = "example-test";
8-
nodes.machine1 = _: {
8+
defaults = {
99
imports = [self.nixosModules.nixos-cli];
1010
services.nixos-cli.enable = true;
11+
services.nixos-cli.useActivationInterface = true;
12+
services.nixos-cli.magic-rollback.enable = true;
1113
};
14+
nodes.machine1 = _: {};
1215

1316
testScript = ''
1417
machine1.succeed("nixos features")

0 commit comments

Comments
 (0)