nixos-rebuild-ng: validate NixOS configuration path (#418243)
This commit is contained in:
@@ -5,11 +5,12 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError, run
|
||||
from textwrap import dedent
|
||||
from typing import Final, assert_never
|
||||
|
||||
from . import nix, tmpdir
|
||||
from .constants import EXECUTABLE, WITH_NIX_2_18, WITH_REEXEC, WITH_SHELL_FILES
|
||||
from .models import Action, BuildAttr, Flake, ImageVariants, NRError, Profile
|
||||
from .models import Action, BuildAttr, Flake, ImageVariants, NixOSRebuildError, Profile
|
||||
from .process import Remote, cleanup_ssh
|
||||
from .utils import Args, LogFormatter, tabulate
|
||||
|
||||
@@ -99,7 +100,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
"--attr",
|
||||
"-A",
|
||||
help="Enable and build the NixOS system from nix file and use the "
|
||||
+ "specified attribute path from file specified by the --file option",
|
||||
"specified attribute path from file specified by the --file option",
|
||||
)
|
||||
main_parser.add_argument(
|
||||
"--flake",
|
||||
@@ -117,7 +118,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
"--install-bootloader",
|
||||
action="store_true",
|
||||
help="Causes the boot loader to be (re)installed on the device specified "
|
||||
+ "by the relevant configuration options",
|
||||
"by the relevant configuration options",
|
||||
)
|
||||
main_parser.add_argument(
|
||||
"--install-grub",
|
||||
@@ -142,7 +143,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
"--upgrade",
|
||||
action="store_true",
|
||||
help="Update the root user's channel named 'nixos' before rebuilding "
|
||||
+ "the system and channels which have a file named '.update-on-nixos-rebuild'",
|
||||
"the system and channels which have a file named '.update-on-nixos-rebuild'",
|
||||
)
|
||||
main_parser.add_argument(
|
||||
"--upgrade-all",
|
||||
@@ -186,7 +187,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
|
||||
main_parser.add_argument(
|
||||
"--image-variant",
|
||||
help="Selects an image variant to build from the "
|
||||
+ "config.system.build.images attribute of the given configuration",
|
||||
"config.system.build.images attribute of the given configuration",
|
||||
)
|
||||
main_parser.add_argument("action", choices=Action.values(), nargs="?")
|
||||
|
||||
@@ -321,7 +322,7 @@ def reexec(
|
||||
# - Exec format error (e.g.: another OS/CPU arch)
|
||||
logger.warning(
|
||||
"could not re-exec in a newer version of nixos-rebuild, "
|
||||
+ "using current version",
|
||||
"using current version",
|
||||
exc_info=logger.isEnabledFor(logging.DEBUG),
|
||||
)
|
||||
# We already run clean-up, let's re-exec in the current version
|
||||
@@ -329,6 +330,37 @@ def reexec(
|
||||
os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
|
||||
|
||||
|
||||
def validate_image_variant(image_variant: str, variants: ImageVariants) -> None:
|
||||
if image_variant not in variants:
|
||||
raise NixOSRebuildError(
|
||||
"please specify one of the following supported image variants via "
|
||||
"--image-variant:\n" + "\n".join(f"- {v}" for v in variants)
|
||||
)
|
||||
|
||||
|
||||
def validate_nixos_config(path_to_config: Path) -> None:
|
||||
if not (path_to_config / "nixos-version").exists() and not os.environ.get(
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
|
||||
):
|
||||
msg = dedent(
|
||||
# the lowercase for the first letter below is proposital
|
||||
f"""
|
||||
your NixOS configuration path seems to be missing essential files.
|
||||
To avoid corrupting your current NixOS installation, the activation will abort.
|
||||
|
||||
This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
|
||||
This is the evaluated NixOS configuration path: {path_to_config}.
|
||||
Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
|
||||
|
||||
If you think this is a mistake, you can set the environment variable
|
||||
NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
|
||||
and re-run the command to continue.
|
||||
Please open an issue if this is the case.
|
||||
"""
|
||||
).strip()
|
||||
raise NixOSRebuildError(msg)
|
||||
|
||||
|
||||
def execute(argv: list[str]) -> None:
|
||||
args, args_groups = parse_args(argv)
|
||||
|
||||
@@ -393,28 +425,20 @@ def execute(argv: list[str]) -> None:
|
||||
no_link = action in (Action.SWITCH, Action.BOOT)
|
||||
rollback = bool(args.rollback)
|
||||
|
||||
def validate_image_variant(variants: ImageVariants) -> None:
|
||||
if args.image_variant not in variants:
|
||||
raise NRError(
|
||||
"please specify one of the following "
|
||||
+ "supported image variants via --image-variant:\n"
|
||||
+ "\n".join(f"- {v}" for v in variants)
|
||||
)
|
||||
|
||||
match action:
|
||||
case Action.BUILD_IMAGE if flake:
|
||||
variants = nix.get_build_image_variants_flake(
|
||||
flake,
|
||||
eval_flags=flake_common_flags,
|
||||
)
|
||||
validate_image_variant(variants)
|
||||
validate_image_variant(args.image_variant, variants)
|
||||
attr = f"config.system.build.images.{args.image_variant}"
|
||||
case Action.BUILD_IMAGE:
|
||||
variants = nix.get_build_image_variants(
|
||||
build_attr,
|
||||
instantiate_flags=common_flags,
|
||||
)
|
||||
validate_image_variant(variants)
|
||||
validate_image_variant(args.image_variant, variants)
|
||||
attr = f"config.system.build.images.{args.image_variant}"
|
||||
case Action.BUILD_VM:
|
||||
attr = "config.system.build.vm"
|
||||
@@ -435,9 +459,11 @@ def execute(argv: list[str]) -> None:
|
||||
if maybe_path_to_config: # kinda silly but this makes mypy happy
|
||||
path_to_config = maybe_path_to_config
|
||||
else:
|
||||
raise NRError("could not find previous generation")
|
||||
raise NixOSRebuildError("could not find previous generation")
|
||||
case (_, True, _, _):
|
||||
raise NRError(f"--rollback is incompatible with '{action}'")
|
||||
raise NixOSRebuildError(
|
||||
f"--rollback is incompatible with '{action}'"
|
||||
)
|
||||
case (_, False, Remote(_), Flake(_)):
|
||||
path_to_config = nix.build_remote_flake(
|
||||
attr,
|
||||
@@ -488,6 +514,7 @@ def execute(argv: list[str]) -> None:
|
||||
copy_flags=copy_flags,
|
||||
)
|
||||
if action in (Action.SWITCH, Action.BOOT):
|
||||
validate_nixos_config(path_to_config)
|
||||
nix.set_profile(
|
||||
profile,
|
||||
path_to_config,
|
||||
|
||||
@@ -4,14 +4,14 @@ import subprocess
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, ClassVar, Self, TypedDict, override
|
||||
from typing import Any, ClassVar, Self, TypedDict, override
|
||||
|
||||
from .process import Remote, run_wrapper
|
||||
|
||||
type ImageVariants = list[str]
|
||||
|
||||
|
||||
class NRError(Exception):
|
||||
class NixOSRebuildError(Exception):
|
||||
"nixos-rebuild general error."
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
@@ -100,6 +100,20 @@ def discover_closest_flake(location: Path) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_hostname(target_host: Remote | None) -> str | None:
|
||||
if target_host:
|
||||
try:
|
||||
return run_wrapper(
|
||||
["uname", "-n"],
|
||||
capture_output=True,
|
||||
remote=target_host,
|
||||
).stdout.strip()
|
||||
except (AttributeError, subprocess.CalledProcessError):
|
||||
return None
|
||||
else:
|
||||
return platform.node()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Flake:
|
||||
path: Path | str
|
||||
@@ -114,15 +128,13 @@ class Flake:
|
||||
return f"{self.path}#{self.attr}"
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
flake_str: str,
|
||||
hostname_fn: Callable[[], str | None] = lambda: None,
|
||||
) -> Self:
|
||||
def parse(cls, flake_str: str, target_host: Remote | None = None) -> Self:
|
||||
m = cls._re.match(flake_str)
|
||||
assert m is not None, f"got no matches for {flake_str}"
|
||||
attr = m.group("attr")
|
||||
nixos_attr = f'nixosConfigurations."{attr or hostname_fn() or "default"}"'
|
||||
nixos_attr = (
|
||||
f'nixosConfigurations."{attr or get_hostname(target_host) or "default"}"'
|
||||
)
|
||||
path_str = m.group("path")
|
||||
if ":" in path_str:
|
||||
return cls(path_str, nixos_attr)
|
||||
@@ -143,24 +155,11 @@ class Flake:
|
||||
|
||||
@classmethod
|
||||
def from_arg(cls, flake_arg: Any, target_host: Remote | None) -> Self | None:
|
||||
def get_hostname() -> str | None:
|
||||
if target_host:
|
||||
try:
|
||||
return run_wrapper(
|
||||
["uname", "-n"],
|
||||
stdout=subprocess.PIPE,
|
||||
remote=target_host,
|
||||
).stdout.strip()
|
||||
except (AttributeError, subprocess.CalledProcessError):
|
||||
return None
|
||||
else:
|
||||
return platform.node()
|
||||
|
||||
match flake_arg:
|
||||
case str(s):
|
||||
return cls.parse(s, get_hostname)
|
||||
return cls.parse(s, target_host)
|
||||
case True:
|
||||
return cls.parse(".", get_hostname)
|
||||
return cls.parse(".", target_host)
|
||||
case False:
|
||||
return None
|
||||
case _:
|
||||
@@ -169,7 +168,7 @@ class Flake:
|
||||
if default_path.exists():
|
||||
# It can be a symlink to the actual flake.
|
||||
default_path = default_path.resolve()
|
||||
return cls.parse(str(default_path.parent), get_hostname)
|
||||
return cls.parse(str(default_path.parent), target_host)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from .models import (
|
||||
Generation,
|
||||
GenerationJson,
|
||||
ImageVariants,
|
||||
NRError,
|
||||
NixOSRebuildError,
|
||||
Profile,
|
||||
Remote,
|
||||
)
|
||||
@@ -256,7 +256,7 @@ def edit(flake: Flake | None, flake_flags: Args | None = None) -> None:
|
||||
)
|
||||
else:
|
||||
if flake_flags:
|
||||
raise NRError("'edit' does not support extra Nix flags")
|
||||
raise NixOSRebuildError("'edit' does not support extra Nix flags")
|
||||
nixos_config = Path(
|
||||
os.getenv("NIXOS_CONFIG") or find_file("nixos-config") or "/etc/nixos"
|
||||
)
|
||||
@@ -266,7 +266,7 @@ def edit(flake: Flake | None, flake_flags: Args | None = None) -> None:
|
||||
if nixos_config.exists():
|
||||
run_wrapper([os.getenv("EDITOR", "nano"), nixos_config], check=False)
|
||||
else:
|
||||
raise NRError("cannot find NixOS config file")
|
||||
raise NixOSRebuildError("cannot find NixOS config file")
|
||||
|
||||
|
||||
def find_file(file: str, nix_flags: Args | None = None) -> Path | None:
|
||||
@@ -424,7 +424,7 @@ def get_generations(profile: Profile) -> list[Generation]:
|
||||
and if this is the current active profile or not.
|
||||
"""
|
||||
if not profile.path.exists():
|
||||
raise NRError(f"no profile '{profile.name}' found")
|
||||
raise NixOSRebuildError(f"no profile '{profile.name}' found")
|
||||
|
||||
def parse_path(path: Path, profile: Profile) -> Generation:
|
||||
entry_id = path.name.split("-")[1]
|
||||
@@ -456,7 +456,7 @@ def get_generations_from_nix_env(
|
||||
and if this is the current active profile or not.
|
||||
"""
|
||||
if not profile.path.exists():
|
||||
raise NRError(f"no profile '{profile.name}' found")
|
||||
raise NixOSRebuildError(f"no profile '{profile.name}' found")
|
||||
|
||||
# Using `nix-env --list-generations` needs root to lock the profile
|
||||
r = run_wrapper(
|
||||
@@ -635,13 +635,13 @@ def switch_to_configuration(
|
||||
"""
|
||||
if specialisation:
|
||||
if action not in (Action.SWITCH, Action.TEST):
|
||||
raise NRError(
|
||||
raise NixOSRebuildError(
|
||||
"'--specialisation' can only be used with 'switch' and 'test'"
|
||||
)
|
||||
path_to_config = path_to_config / f"specialisation/{specialisation}"
|
||||
|
||||
if not path_to_config.exists():
|
||||
raise NRError(f"specialisation not found: {specialisation}")
|
||||
raise NixOSRebuildError(f"specialisation not found: {specialisation}")
|
||||
|
||||
r = run_wrapper(
|
||||
["test", "-d", "/run/systemd/system"],
|
||||
@@ -652,7 +652,7 @@ def switch_to_configuration(
|
||||
if r.returncode:
|
||||
logger.debug(
|
||||
"skipping systemd-run to switch configuration since systemd is "
|
||||
+ "not working in target host"
|
||||
"not working in target host"
|
||||
)
|
||||
cmd = []
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ class Remote:
|
||||
if o in ["-t", "-tt", "RequestTTY=yes", "RequestTTY=force"]:
|
||||
logger.warning(
|
||||
f"detected option '{o}' in NIX_SSHOPTS. SSH's TTY may "
|
||||
+ "cause issues, it is recommended to remove this option"
|
||||
"cause issues, it is recommended to remove this option"
|
||||
)
|
||||
if not ask_sudo_password:
|
||||
logger.warning(
|
||||
"if you want to prompt for sudo password use "
|
||||
+ "'--ask-sudo-password' option instead"
|
||||
"'--ask-sudo-password' option instead"
|
||||
)
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ def run_wrapper(
|
||||
if sudo and remote and remote.sudo_password is None:
|
||||
logger.error(
|
||||
"while running command with remote sudo, did you forget to use "
|
||||
+ "--ask-sudo-password?"
|
||||
"--ask-sudo-password?"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -39,37 +39,38 @@ ignore_missing_imports = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
extend-select = [
|
||||
# Enforce type annotations
|
||||
# enforce type annotations
|
||||
"ANN",
|
||||
# don't shadow built-in names
|
||||
"A",
|
||||
# Better list/set/dict comprehensions
|
||||
# better list/set/dict comprehensions
|
||||
"C4",
|
||||
# Check for debugger statements
|
||||
# check for debugger statements
|
||||
"T10",
|
||||
# ensure imports are sorted
|
||||
"I",
|
||||
# Automatically upgrade syntax for newer versions
|
||||
# automatically upgrade syntax for newer versions
|
||||
"UP",
|
||||
# detect common sources of bugs
|
||||
"B",
|
||||
# Ruff specific rules
|
||||
# ruff specific rules
|
||||
"RUF",
|
||||
# require `check` argument for `subprocess.run`
|
||||
"PLW1510",
|
||||
# check for needless exception names in raise statements
|
||||
"TRY201",
|
||||
# Pythonic naming conventions
|
||||
# pythonic naming conventions
|
||||
"N",
|
||||
# string concatenation rules
|
||||
"ISC001",
|
||||
"ISC002",
|
||||
"ISC003",
|
||||
]
|
||||
ignore = [
|
||||
# allow Any type
|
||||
"ANN401"
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/" = ["FA102"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = ["."]
|
||||
addopts = "--import-mode=importlib"
|
||||
|
||||
@@ -213,7 +213,11 @@ def test_reexec_flake(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@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_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
|
||||
nixpkgs_path = tmp_path / "nixpkgs"
|
||||
@@ -291,7 +295,15 @@ def test_execute_nix_boot(mock_run: Mock, tmp_path: Path) -> None:
|
||||
"boot",
|
||||
],
|
||||
check=True,
|
||||
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "0"}}),
|
||||
**(
|
||||
DEFAULT_RUN_KWARGS
|
||||
| {
|
||||
"env": {
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -421,7 +433,11 @@ def test_execute_nix_build_image_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@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_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
config_path = tmp_path / "test"
|
||||
@@ -498,13 +514,25 @@ def test_execute_nix_switch_flake(mock_run: Mock, tmp_path: Path) -> None:
|
||||
"switch",
|
||||
],
|
||||
check=True,
|
||||
**(DEFAULT_RUN_KWARGS | {"env": {"NIXOS_INSTALL_BOOTLOADER": "1"}}),
|
||||
**(
|
||||
DEFAULT_RUN_KWARGS
|
||||
| {
|
||||
"env": {
|
||||
"NIXOS_INSTALL_BOOTLOADER": "1",
|
||||
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1",
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch("uuid.uuid4", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
@@ -714,7 +742,11 @@ def test_execute_nix_switch_build_target_host(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
def test_execute_nix_switch_flake_target_host(
|
||||
@@ -817,7 +849,11 @@ def test_execute_nix_switch_flake_target_host(
|
||||
)
|
||||
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM": "1"},
|
||||
clear=True,
|
||||
)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
@patch(get_qualified_name(nr.cleanup_ssh), autospec=True)
|
||||
def test_execute_nix_switch_flake_build_host(
|
||||
|
||||
@@ -30,16 +30,23 @@ def test_build_attr_to_attr() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_flake_parse(tmpdir: Path, monkeypatch: MonkeyPatch) -> None:
|
||||
@patch("platform.node", autospec=True, return_value="hostname")
|
||||
def test_flake_parse(mock_node: Mock, tmpdir: Path, monkeypatch: MonkeyPatch) -> None:
|
||||
assert m.Flake.parse("/path/to/flake#attr") == m.Flake(
|
||||
Path("/path/to/flake"), 'nixosConfigurations."attr"'
|
||||
)
|
||||
assert m.Flake.parse("/path/ to /flake", lambda: "hostname") == m.Flake(
|
||||
assert m.Flake.parse("/path/ to /flake") == m.Flake(
|
||||
Path("/path/ to /flake"), 'nixosConfigurations."hostname"'
|
||||
)
|
||||
assert m.Flake.parse("/path/to/flake", lambda: "hostname") == m.Flake(
|
||||
Path("/path/to/flake"), 'nixosConfigurations."hostname"'
|
||||
)
|
||||
with patch(
|
||||
get_qualified_name(m.run_wrapper, m),
|
||||
autospec=True,
|
||||
return_value=subprocess.CompletedProcess([], 0, stdout="remote\n"),
|
||||
):
|
||||
target_host = m.Remote("target@remote", [], None)
|
||||
assert m.Flake.parse("/path/to/flake", target_host) == m.Flake(
|
||||
Path("/path/to/flake"), 'nixosConfigurations."remote"'
|
||||
)
|
||||
# change directory to tmpdir
|
||||
with monkeypatch.context() as patch_context:
|
||||
patch_context.chdir(tmpdir)
|
||||
@@ -49,10 +56,16 @@ def test_flake_parse(tmpdir: Path, monkeypatch: MonkeyPatch) -> None:
|
||||
assert m.Flake.parse("#attr") == m.Flake(
|
||||
Path("."), 'nixosConfigurations."attr"'
|
||||
)
|
||||
assert m.Flake.parse(".") == m.Flake(Path("."), 'nixosConfigurations."default"')
|
||||
assert m.Flake.parse(".") == m.Flake(
|
||||
Path("."), 'nixosConfigurations."hostname"'
|
||||
)
|
||||
assert m.Flake.parse("path:/to/flake#attr") == m.Flake(
|
||||
"path:/to/flake", 'nixosConfigurations."attr"'
|
||||
)
|
||||
|
||||
# from here on we should return "default"
|
||||
mock_node.return_value = None
|
||||
|
||||
assert m.Flake.parse("github:user/repo/branch") == m.Flake(
|
||||
"github:user/repo/branch", 'nixosConfigurations."default"'
|
||||
)
|
||||
|
||||
@@ -714,7 +714,7 @@ def test_switch_to_configuration_without_systemd_run(
|
||||
remote=None,
|
||||
)
|
||||
|
||||
with pytest.raises(m.NRError) as e:
|
||||
with pytest.raises(m.NixOSRebuildError) as e:
|
||||
n.switch_to_configuration(
|
||||
config_path,
|
||||
m.Action.BOOT,
|
||||
|
||||
Reference in New Issue
Block a user