nixos-rebuild-ng: allow deploying closures (#482430)
This commit is contained in:
@@ -1107,6 +1107,9 @@ in
|
||||
nixos-rebuild-specialisations = runTestOn [ "x86_64-linux" ] {
|
||||
imports = [ ./nixos-rebuild-specialisations.nix ];
|
||||
};
|
||||
nixos-rebuild-store-path = runTestOn [ "x86_64-linux" ] {
|
||||
imports = [ ./nixos-rebuild-store-path.nix ];
|
||||
};
|
||||
nixos-rebuild-target-host = runTest {
|
||||
imports = [ ./nixos-rebuild-target-host.nix ];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
{ hostPkgs, ... }:
|
||||
{
|
||||
name = "nixos-rebuild-store-path";
|
||||
|
||||
# TODO: remove overlay from nixos/modules/profiles/installation-device.nix
|
||||
# make it a _small package instead, then remove pkgsReadOnly = false;.
|
||||
node.pkgsReadOnly = false;
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../modules/profiles/installation-device.nix
|
||||
../modules/profiles/base.nix
|
||||
];
|
||||
|
||||
nix.settings = {
|
||||
substituters = lib.mkForce [ ];
|
||||
hashed-mirrors = null;
|
||||
connect-timeout = 1;
|
||||
};
|
||||
|
||||
system.includeBuildDependencies = true;
|
||||
|
||||
system.extraDependencies = [
|
||||
# Not part of the initial build apparently?
|
||||
pkgs.grub2
|
||||
];
|
||||
|
||||
system.switch.enable = true;
|
||||
|
||||
virtualisation = {
|
||||
cores = 2;
|
||||
memorySize = 4096;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
let
|
||||
configFile =
|
||||
hostname:
|
||||
hostPkgs.writeText "configuration.nix" # nix
|
||||
''
|
||||
{ lib, pkgs, ... }: {
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
<nixpkgs/nixos/modules/testing/test-instrumentation.nix>
|
||||
];
|
||||
|
||||
boot.loader.grub = {
|
||||
enable = true;
|
||||
device = "/dev/vda";
|
||||
forceInstall = true;
|
||||
};
|
||||
|
||||
documentation.enable = false;
|
||||
|
||||
networking.hostName = "${hostname}";
|
||||
}
|
||||
'';
|
||||
|
||||
in
|
||||
# python
|
||||
''
|
||||
machine.start()
|
||||
machine.succeed("udevadm settle")
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
machine.succeed("nixos-generate-config")
|
||||
|
||||
with subtest("Build configuration without switching"):
|
||||
machine.copy_from_host(
|
||||
"${configFile "store-path-test"}",
|
||||
"/etc/nixos/configuration.nix",
|
||||
)
|
||||
store_path = machine.succeed("nix-build '<nixpkgs/nixos>' -A system --no-out-link").strip()
|
||||
machine.succeed(f"test -f {store_path}/nixos-version")
|
||||
|
||||
with subtest("Switch using --store-path"):
|
||||
machine.succeed(f"nixos-rebuild switch --store-path {store_path}")
|
||||
hostname = machine.succeed("cat /etc/hostname").strip()
|
||||
assert hostname == "store-path-test", f"Expected hostname 'store-path-test', got '{hostname}'"
|
||||
|
||||
with subtest("Test using --store-path"):
|
||||
machine.copy_from_host(
|
||||
"${configFile "store-path-test-2"}",
|
||||
"/etc/nixos/configuration.nix",
|
||||
)
|
||||
store_path_2 = machine.succeed("nix-build '<nixpkgs/nixos>' -A system --no-out-link").strip()
|
||||
machine.succeed(f"nixos-rebuild test --store-path {store_path_2}")
|
||||
hostname = machine.succeed("cat /etc/hostname").strip()
|
||||
assert hostname == "store-path-test-2", f"Expected hostname 'store-path-test-2', got '{hostname}'"
|
||||
|
||||
with subtest("Ensure --store-path rejects invalid combinations"):
|
||||
machine.fail(f"nixos-rebuild switch --store-path {store_path} --rollback")
|
||||
machine.fail(f"nixos-rebuild switch --store-path {store_path} --flake .")
|
||||
machine.fail(f"nixos-rebuild build --store-path {store_path}")
|
||||
'';
|
||||
}
|
||||
@@ -21,7 +21,7 @@ nixos-rebuild - reconfigure a NixOS machine
|
||||
_nixos-rebuild_ \[--verbose] [--quiet] [--max-jobs MAX_JOBS] [--cores CORES] [--log-format LOG_FORMAT] [--keep-going] [--keep-failed] [--fallback] [--repair] [--option OPTION OPTION] [--builders BUILDERS] [--include INCLUDE]++
|
||||
\[--print-build-logs] [--show-trace] [--accept-flake-config] [--refresh] [--impure] [--offline] [--no-net] [--recreate-lock-file] [--no-update-lock-file] [--no-write-lock-file] [--no-registries] [--commit-lock-file]++
|
||||
\[--update-input UPDATE_INPUT] [--override-input OVERRIDE_INPUT OVERRIDE_INPUT] [--no-build-output] [--use-substitutes] [--help] [--debug] [--file FILE] [--attr ATTR] [--flake [FLAKE]] [--no-flake] [--install-bootloader]++
|
||||
\[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++
|
||||
\[--profile-name PROFILE_NAME] [--specialisation SPECIALISATION] [--rollback] [--store-path STORE_PATH] [--upgrade] [--upgrade-all] [--json] [--ask-sudo-password] [--sudo] [--no-reexec]++
|
||||
\[--build-host BUILD_HOST] [--target-host TARGET_HOST] [--no-build-nix] [--image-variant IMAGE_VARIANT]++
|
||||
\[{switch,boot,test,build,edit,repl,dry-build,dry-run,dry-activate,build-image,build-vm,build-vm-with-bootloader,list-generations}]
|
||||
|
||||
@@ -182,6 +182,20 @@ It must be one of the following:
|
||||
(The previous configuration is defined as the one before the “current”
|
||||
generation of the Nix profile _/nix/var/nix/profiles/system_.)
|
||||
|
||||
*--store-path* _path_
|
||||
Use a pre-built NixOS system store path at _path_ instead of evaluating
|
||||
and building from the configuration. This skips the evaluation and build
|
||||
phases entirely. The path must be a valid NixOS system closure
|
||||
(containing _nixos-version_ and _bin/switch-to-configuration_).
|
||||
|
||||
This is useful for deploying closures that were built elsewhere, such as
|
||||
in CI systems or on remote build machines.
|
||||
|
||||
Can only be used with *switch*, *boot*, *test*, and *dry-activate*
|
||||
actions. Mutually exclusive with *--rollback*, *--flake*, *--file*, and
|
||||
*--attr*. The *--build-host* option is ignored when *--store-path* is
|
||||
specified.
|
||||
|
||||
*--builders* _builder-spec_
|
||||
Allow ad-hoc remote builders for building the new system. This requires
|
||||
the user executing *nixos-rebuild* (usually root) to be configured as a
|
||||
|
||||
@@ -98,6 +98,7 @@ python3Packages.buildPythonApplication rec {
|
||||
# FIXME: this test is disabled since it times out in @ofborg
|
||||
# nixos-rebuild-install-bootloader
|
||||
nixos-rebuild-specialisations
|
||||
nixos-rebuild-store-path
|
||||
nixos-rebuild-target-host
|
||||
;
|
||||
repl = callPackage ./tests/repl.nix { };
|
||||
|
||||
@@ -136,6 +136,11 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
action="store_true",
|
||||
help="Roll back to the previous configuration",
|
||||
)
|
||||
main_parser.add_argument(
|
||||
"--store-path",
|
||||
metavar="PATH",
|
||||
help="Use a pre-built NixOS system store path instead of building",
|
||||
)
|
||||
main_parser.add_argument(
|
||||
"--upgrade",
|
||||
action="store_true",
|
||||
@@ -269,6 +274,22 @@ def parse_args(
|
||||
if args.flake and (args.file or args.attr):
|
||||
parser.error("--flake cannot be used with --file or --attr")
|
||||
|
||||
if args.store_path:
|
||||
if args.rollback:
|
||||
parser.error("--store-path and --rollback are mutually exclusive")
|
||||
if args.flake or args.file or args.attr:
|
||||
parser.error("--store-path cannot be used with --flake, --file, or --attr")
|
||||
if args.action not in (
|
||||
Action.SWITCH.value,
|
||||
Action.BOOT.value,
|
||||
Action.TEST.value,
|
||||
Action.DRY_ACTIVATE.value,
|
||||
):
|
||||
parser.error(f"--store-path cannot be used with '{args.action}'")
|
||||
if args.flake is None:
|
||||
# Disable flake auto-detection since we're using a pre-built store path
|
||||
args.flake = False
|
||||
|
||||
return args, grouped_nix_args
|
||||
|
||||
|
||||
@@ -297,7 +318,7 @@ def execute(argv: list[str]) -> None:
|
||||
build_attr = BuildAttr.from_arg(args.attr, args.file)
|
||||
flake = Flake.from_arg(args.flake, target_host)
|
||||
|
||||
if can_run and not flake:
|
||||
if can_run and not flake and not args.store_path:
|
||||
services.write_version_suffix(grouped_nix_args)
|
||||
|
||||
match action:
|
||||
|
||||
@@ -290,7 +290,14 @@ def build_and_activate_system(
|
||||
grouped_nix_args=grouped_nix_args,
|
||||
)
|
||||
|
||||
if args.rollback:
|
||||
if args.store_path:
|
||||
path_to_config = Path(args.store_path)
|
||||
nix.copy_closure(
|
||||
path_to_config,
|
||||
to_host=target_host,
|
||||
copy_flags=grouped_nix_args.copy_flags,
|
||||
)
|
||||
elif args.rollback:
|
||||
path_to_config = _rollback_system(
|
||||
action=action,
|
||||
args=args,
|
||||
|
||||
@@ -34,6 +34,30 @@ def test_parse_args() -> None:
|
||||
nr.parse_args(["nixos-rebuild", "edit", "--attr", "attr"])
|
||||
assert e.value.code == 2
|
||||
|
||||
# --store-path validation tests
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(
|
||||
["nixos-rebuild", "switch", "--store-path", "/nix/store/foo", "--rollback"]
|
||||
)
|
||||
assert e.value.code == 2
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(
|
||||
["nixos-rebuild", "switch", "--store-path", "/nix/store/foo", "--flake"]
|
||||
)
|
||||
assert e.value.code == 2
|
||||
|
||||
with pytest.raises(SystemExit) as e:
|
||||
nr.parse_args(["nixos-rebuild", "build", "--store-path", "/nix/store/foo"])
|
||||
assert e.value.code == 2
|
||||
|
||||
# --store-path should disable flake auto-detection
|
||||
r_store_path, _ = nr.parse_args(
|
||||
["nixos-rebuild", "switch", "--store-path", "/nix/store/foo"]
|
||||
)
|
||||
assert r_store_path.flake is False
|
||||
assert r_store_path.store_path == "/nix/store/foo"
|
||||
|
||||
r1, g1 = nr.parse_args(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
@@ -1214,3 +1238,163 @@ def test_execute_test_rollback(
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
def test_execute_switch_store_path(mock_run: Mock, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test-system"
|
||||
config_path.mkdir()
|
||||
|
||||
mock_run.return_value = CompletedProcess([], 0)
|
||||
|
||||
nr.execute(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"switch",
|
||||
"--store-path",
|
||||
str(config_path),
|
||||
"--no-reexec",
|
||||
]
|
||||
)
|
||||
|
||||
# --store-path skips build and write_version_suffix, so only activation calls
|
||||
assert mock_run.call_count == 3
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
[
|
||||
"nix-env",
|
||||
"-p",
|
||||
Path("/nix/var/nix/profiles/system"),
|
||||
"--set",
|
||||
config_path,
|
||||
],
|
||||
check=True,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
["test", "-d", "/run/systemd/system"],
|
||||
check=False,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
*nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX,
|
||||
config_path / "bin/switch-to-configuration",
|
||||
"switch",
|
||||
],
|
||||
check=True,
|
||||
**(
|
||||
DEFAULT_RUN_KWARGS
|
||||
| {
|
||||
"env": {
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch(get_qualified_name(nr.services.cleanup_ssh), autospec=True)
|
||||
def test_execute_switch_store_path_target_host(
|
||||
mock_cleanup_ssh: Mock,
|
||||
mock_run: Mock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config_path = tmp_path / "test-system"
|
||||
config_path.mkdir()
|
||||
|
||||
mock_run.return_value = CompletedProcess([], 0)
|
||||
|
||||
nr.execute(
|
||||
[
|
||||
"nixos-rebuild",
|
||||
"switch",
|
||||
"--store-path",
|
||||
str(config_path),
|
||||
"--target-host",
|
||||
"user@remote-host",
|
||||
"--sudo",
|
||||
"--no-reexec",
|
||||
]
|
||||
)
|
||||
|
||||
# --store-path skips build and write_version_suffix, so only copy/activation calls
|
||||
assert mock_run.call_count == 5
|
||||
mock_run.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
["nix-copy-closure", "--to", "user@remote-host", config_path],
|
||||
check=True,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"ssh",
|
||||
*nr.process.SSH_DEFAULT_OPTS,
|
||||
"user@remote-host",
|
||||
"--",
|
||||
"test",
|
||||
"-f",
|
||||
str(config_path / "nixos-version"),
|
||||
],
|
||||
check=False,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"ssh",
|
||||
*nr.process.SSH_DEFAULT_OPTS,
|
||||
"user@remote-host",
|
||||
"--",
|
||||
"sudo",
|
||||
"nix-env",
|
||||
"-p",
|
||||
"/nix/var/nix/profiles/system",
|
||||
"--set",
|
||||
str(config_path),
|
||||
],
|
||||
check=True,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"ssh",
|
||||
*nr.process.SSH_DEFAULT_OPTS,
|
||||
"user@remote-host",
|
||||
"--",
|
||||
"test",
|
||||
"-d",
|
||||
"/run/systemd/system",
|
||||
],
|
||||
check=False,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
call(
|
||||
[
|
||||
"ssh",
|
||||
*nr.process.SSH_DEFAULT_OPTS,
|
||||
"user@remote-host",
|
||||
"--",
|
||||
"sudo",
|
||||
"env",
|
||||
"NIXOS_INSTALL_BOOTLOADER=0",
|
||||
*nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX,
|
||||
str(config_path / "bin/switch-to-configuration"),
|
||||
"switch",
|
||||
],
|
||||
check=True,
|
||||
**DEFAULT_RUN_KWARGS,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user