Skip to content

Commit ae48ab3

Browse files
authored
nixos-rebuild-ng: validate NixOS configuration path (#418243)
2 parents 662f1d7 + e364976 commit ae48ab3

File tree

8 files changed

+152
-76
lines changed

8 files changed

+152
-76
lines changed

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/__init__.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
import sys
66
from pathlib import Path
77
from subprocess import CalledProcessError, run
8+
from textwrap import dedent
89
from typing import Final, assert_never
910

1011
from . import nix, tmpdir
1112
from .constants import EXECUTABLE, WITH_NIX_2_18, WITH_REEXEC, WITH_SHELL_FILES
12-
from .models import Action, BuildAttr, Flake, ImageVariants, NRError, Profile
13+
from .models import Action, BuildAttr, Flake, ImageVariants, NixOSRebuildError, Profile
1314
from .process import Remote, cleanup_ssh
1415
from .utils import Args, LogFormatter, tabulate
1516

@@ -99,7 +100,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
99100
"--attr",
100101
"-A",
101102
help="Enable and build the NixOS system from nix file and use the "
102-
+ "specified attribute path from file specified by the --file option",
103+
"specified attribute path from file specified by the --file option",
103104
)
104105
main_parser.add_argument(
105106
"--flake",
@@ -117,7 +118,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
117118
"--install-bootloader",
118119
action="store_true",
119120
help="Causes the boot loader to be (re)installed on the device specified "
120-
+ "by the relevant configuration options",
121+
"by the relevant configuration options",
121122
)
122123
main_parser.add_argument(
123124
"--install-grub",
@@ -142,7 +143,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
142143
"--upgrade",
143144
action="store_true",
144145
help="Update the root user's channel named 'nixos' before rebuilding "
145-
+ "the system and channels which have a file named '.update-on-nixos-rebuild'",
146+
"the system and channels which have a file named '.update-on-nixos-rebuild'",
146147
)
147148
main_parser.add_argument(
148149
"--upgrade-all",
@@ -186,7 +187,7 @@ def get_parser() -> tuple[argparse.ArgumentParser, dict[str, argparse.ArgumentPa
186187
main_parser.add_argument(
187188
"--image-variant",
188189
help="Selects an image variant to build from the "
189-
+ "config.system.build.images attribute of the given configuration",
190+
"config.system.build.images attribute of the given configuration",
190191
)
191192
main_parser.add_argument("action", choices=Action.values(), nargs="?")
192193

@@ -321,14 +322,45 @@ def reexec(
321322
# - Exec format error (e.g.: another OS/CPU arch)
322323
logger.warning(
323324
"could not re-exec in a newer version of nixos-rebuild, "
324-
+ "using current version",
325+
"using current version",
325326
exc_info=logger.isEnabledFor(logging.DEBUG),
326327
)
327328
# We already run clean-up, let's re-exec in the current version
328329
# to avoid issues
329330
os.execve(current, argv, os.environ | {"_NIXOS_REBUILD_REEXEC": "1"})
330331

331332

333+
def validate_image_variant(image_variant: str, variants: ImageVariants) -> None:
334+
if image_variant not in variants:
335+
raise NixOSRebuildError(
336+
"please specify one of the following supported image variants via "
337+
"--image-variant:\n" + "\n".join(f"- {v}" for v in variants)
338+
)
339+
340+
341+
def validate_nixos_config(path_to_config: Path) -> None:
342+
if not (path_to_config / "nixos-version").exists() and not os.environ.get(
343+
"NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM"
344+
):
345+
msg = dedent(
346+
# the lowercase for the first letter below is proposital
347+
f"""
348+
your NixOS configuration path seems to be missing essential files.
349+
To avoid corrupting your current NixOS installation, the activation will abort.
350+
351+
This could be caused by Nix bug: https://github.com/NixOS/nix/issues/13367.
352+
This is the evaluated NixOS configuration path: {path_to_config}.
353+
Change the directory to somewhere else (e.g., `cd $HOME`) before trying again.
354+
355+
If you think this is a mistake, you can set the environment variable
356+
NIXOS_REBUILD_I_UNDERSTAND_THE_CONSEQUENCES_PLEASE_BREAK_MY_SYSTEM to 1
357+
and re-run the command to continue.
358+
Please open an issue if this is the case.
359+
"""
360+
).strip()
361+
raise NixOSRebuildError(msg)
362+
363+
332364
def execute(argv: list[str]) -> None:
333365
args, args_groups = parse_args(argv)
334366

@@ -393,28 +425,20 @@ def execute(argv: list[str]) -> None:
393425
no_link = action in (Action.SWITCH, Action.BOOT)
394426
rollback = bool(args.rollback)
395427

396-
def validate_image_variant(variants: ImageVariants) -> None:
397-
if args.image_variant not in variants:
398-
raise NRError(
399-
"please specify one of the following "
400-
+ "supported image variants via --image-variant:\n"
401-
+ "\n".join(f"- {v}" for v in variants)
402-
)
403-
404428
match action:
405429
case Action.BUILD_IMAGE if flake:
406430
variants = nix.get_build_image_variants_flake(
407431
flake,
408432
eval_flags=flake_common_flags,
409433
)
410-
validate_image_variant(variants)
434+
validate_image_variant(args.image_variant, variants)
411435
attr = f"config.system.build.images.{args.image_variant}"
412436
case Action.BUILD_IMAGE:
413437
variants = nix.get_build_image_variants(
414438
build_attr,
415439
instantiate_flags=common_flags,
416440
)
417-
validate_image_variant(variants)
441+
validate_image_variant(args.image_variant, variants)
418442
attr = f"config.system.build.images.{args.image_variant}"
419443
case Action.BUILD_VM:
420444
attr = "config.system.build.vm"
@@ -435,9 +459,11 @@ def validate_image_variant(variants: ImageVariants) -> None:
435459
if maybe_path_to_config: # kinda silly but this makes mypy happy
436460
path_to_config = maybe_path_to_config
437461
else:
438-
raise NRError("could not find previous generation")
462+
raise NixOSRebuildError("could not find previous generation")
439463
case (_, True, _, _):
440-
raise NRError(f"--rollback is incompatible with '{action}'")
464+
raise NixOSRebuildError(
465+
f"--rollback is incompatible with '{action}'"
466+
)
441467
case (_, False, Remote(_), Flake(_)):
442468
path_to_config = nix.build_remote_flake(
443469
attr,
@@ -488,6 +514,7 @@ def validate_image_variant(variants: ImageVariants) -> None:
488514
copy_flags=copy_flags,
489515
)
490516
if action in (Action.SWITCH, Action.BOOT):
517+
validate_nixos_config(path_to_config)
491518
nix.set_profile(
492519
profile,
493520
path_to_config,

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/models.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from dataclasses import dataclass
55
from enum import Enum
66
from pathlib import Path
7-
from typing import Any, Callable, ClassVar, Self, TypedDict, override
7+
from typing import Any, ClassVar, Self, TypedDict, override
88

99
from .process import Remote, run_wrapper
1010

1111
type ImageVariants = list[str]
1212

1313

14-
class NRError(Exception):
14+
class NixOSRebuildError(Exception):
1515
"nixos-rebuild general error."
1616

1717
def __init__(self, message: str) -> None:
@@ -100,6 +100,20 @@ def discover_closest_flake(location: Path) -> Path | None:
100100
return None
101101

102102

103+
def get_hostname(target_host: Remote | None) -> str | None:
104+
if target_host:
105+
try:
106+
return run_wrapper(
107+
["uname", "-n"],
108+
capture_output=True,
109+
remote=target_host,
110+
).stdout.strip()
111+
except (AttributeError, subprocess.CalledProcessError):
112+
return None
113+
else:
114+
return platform.node()
115+
116+
103117
@dataclass(frozen=True)
104118
class Flake:
105119
path: Path | str
@@ -114,15 +128,13 @@ def __str__(self) -> str:
114128
return f"{self.path}#{self.attr}"
115129

116130
@classmethod
117-
def parse(
118-
cls,
119-
flake_str: str,
120-
hostname_fn: Callable[[], str | None] = lambda: None,
121-
) -> Self:
131+
def parse(cls, flake_str: str, target_host: Remote | None = None) -> Self:
122132
m = cls._re.match(flake_str)
123133
assert m is not None, f"got no matches for {flake_str}"
124134
attr = m.group("attr")
125-
nixos_attr = f'nixosConfigurations."{attr or hostname_fn() or "default"}"'
135+
nixos_attr = (
136+
f'nixosConfigurations."{attr or get_hostname(target_host) or "default"}"'
137+
)
126138
path_str = m.group("path")
127139
if ":" in path_str:
128140
return cls(path_str, nixos_attr)
@@ -143,24 +155,11 @@ def parse(
143155

144156
@classmethod
145157
def from_arg(cls, flake_arg: Any, target_host: Remote | None) -> Self | None:
146-
def get_hostname() -> str | None:
147-
if target_host:
148-
try:
149-
return run_wrapper(
150-
["uname", "-n"],
151-
stdout=subprocess.PIPE,
152-
remote=target_host,
153-
).stdout.strip()
154-
except (AttributeError, subprocess.CalledProcessError):
155-
return None
156-
else:
157-
return platform.node()
158-
159158
match flake_arg:
160159
case str(s):
161-
return cls.parse(s, get_hostname)
160+
return cls.parse(s, target_host)
162161
case True:
163-
return cls.parse(".", get_hostname)
162+
return cls.parse(".", target_host)
164163
case False:
165164
return None
166165
case _:
@@ -169,7 +168,7 @@ def get_hostname() -> str | None:
169168
if default_path.exists():
170169
# It can be a symlink to the actual flake.
171170
default_path = default_path.resolve()
172-
return cls.parse(str(default_path.parent), get_hostname)
171+
return cls.parse(str(default_path.parent), target_host)
173172
else:
174173
return None
175174

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/nix.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
Generation,
2121
GenerationJson,
2222
ImageVariants,
23-
NRError,
23+
NixOSRebuildError,
2424
Profile,
2525
Remote,
2626
)
@@ -256,7 +256,7 @@ def edit(flake: Flake | None, flake_flags: Args | None = None) -> None:
256256
)
257257
else:
258258
if flake_flags:
259-
raise NRError("'edit' does not support extra Nix flags")
259+
raise NixOSRebuildError("'edit' does not support extra Nix flags")
260260
nixos_config = Path(
261261
os.getenv("NIXOS_CONFIG") or find_file("nixos-config") or "/etc/nixos"
262262
)
@@ -266,7 +266,7 @@ def edit(flake: Flake | None, flake_flags: Args | None = None) -> None:
266266
if nixos_config.exists():
267267
run_wrapper([os.getenv("EDITOR", "nano"), nixos_config], check=False)
268268
else:
269-
raise NRError("cannot find NixOS config file")
269+
raise NixOSRebuildError("cannot find NixOS config file")
270270

271271

272272
def find_file(file: str, nix_flags: Args | None = None) -> Path | None:
@@ -424,7 +424,7 @@ def get_generations(profile: Profile) -> list[Generation]:
424424
and if this is the current active profile or not.
425425
"""
426426
if not profile.path.exists():
427-
raise NRError(f"no profile '{profile.name}' found")
427+
raise NixOSRebuildError(f"no profile '{profile.name}' found")
428428

429429
def parse_path(path: Path, profile: Profile) -> Generation:
430430
entry_id = path.name.split("-")[1]
@@ -456,7 +456,7 @@ def get_generations_from_nix_env(
456456
and if this is the current active profile or not.
457457
"""
458458
if not profile.path.exists():
459-
raise NRError(f"no profile '{profile.name}' found")
459+
raise NixOSRebuildError(f"no profile '{profile.name}' found")
460460

461461
# Using `nix-env --list-generations` needs root to lock the profile
462462
r = run_wrapper(
@@ -635,13 +635,13 @@ def switch_to_configuration(
635635
"""
636636
if specialisation:
637637
if action not in (Action.SWITCH, Action.TEST):
638-
raise NRError(
638+
raise NixOSRebuildError(
639639
"'--specialisation' can only be used with 'switch' and 'test'"
640640
)
641641
path_to_config = path_to_config / f"specialisation/{specialisation}"
642642

643643
if not path_to_config.exists():
644-
raise NRError(f"specialisation not found: {specialisation}")
644+
raise NixOSRebuildError(f"specialisation not found: {specialisation}")
645645

646646
r = run_wrapper(
647647
["test", "-d", "/run/systemd/system"],
@@ -652,7 +652,7 @@ def switch_to_configuration(
652652
if r.returncode:
653653
logger.debug(
654654
"skipping systemd-run to switch configuration since systemd is "
655-
+ "not working in target host"
655+
"not working in target host"
656656
)
657657
cmd = []
658658

pkgs/by-name/ni/nixos-rebuild-ng/src/nixos_rebuild/process.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ def _validate_opts(opts: list[str], ask_sudo_password: bool | None) -> None:
5555
if o in ["-t", "-tt", "RequestTTY=yes", "RequestTTY=force"]:
5656
logger.warning(
5757
f"detected option '{o}' in NIX_SSHOPTS. SSH's TTY may "
58-
+ "cause issues, it is recommended to remove this option"
58+
"cause issues, it is recommended to remove this option"
5959
)
6060
if not ask_sudo_password:
6161
logger.warning(
6262
"if you want to prompt for sudo password use "
63-
+ "'--ask-sudo-password' option instead"
63+
"'--ask-sudo-password' option instead"
6464
)
6565

6666

@@ -161,7 +161,7 @@ def run_wrapper(
161161
if sudo and remote and remote.sudo_password is None:
162162
logger.error(
163163
"while running command with remote sudo, did you forget to use "
164-
+ "--ask-sudo-password?"
164+
"--ask-sudo-password?"
165165
)
166166
raise
167167

pkgs/by-name/ni/nixos-rebuild-ng/src/pyproject.toml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,37 +39,38 @@ ignore_missing_imports = true
3939

4040
[tool.ruff.lint]
4141
extend-select = [
42-
# Enforce type annotations
42+
# enforce type annotations
4343
"ANN",
4444
# don't shadow built-in names
4545
"A",
46-
# Better list/set/dict comprehensions
46+
# better list/set/dict comprehensions
4747
"C4",
48-
# Check for debugger statements
48+
# check for debugger statements
4949
"T10",
5050
# ensure imports are sorted
5151
"I",
52-
# Automatically upgrade syntax for newer versions
52+
# automatically upgrade syntax for newer versions
5353
"UP",
5454
# detect common sources of bugs
5555
"B",
56-
# Ruff specific rules
56+
# ruff specific rules
5757
"RUF",
5858
# require `check` argument for `subprocess.run`
5959
"PLW1510",
6060
# check for needless exception names in raise statements
6161
"TRY201",
62-
# Pythonic naming conventions
62+
# pythonic naming conventions
6363
"N",
64+
# string concatenation rules
65+
"ISC001",
66+
"ISC002",
67+
"ISC003",
6468
]
6569
ignore = [
6670
# allow Any type
6771
"ANN401"
6872
]
6973

70-
[tool.ruff.lint.per-file-ignores]
71-
"tests/" = ["FA102"]
72-
7374
[tool.pytest.ini_options]
7475
pythonpath = ["."]
7576
addopts = "--import-mode=importlib"

0 commit comments

Comments
 (0)