mirror of
https://github.com/NixOS/nixpkgs.git
synced 2026-06-05 21:03:40 +00:00
nixos-rebuild-ng: add --elevate=run0
Locally this just prefixes activation commands with `run0 --`, so the user's normal polkit agent (graphical or pkttyagent) handles auth. For --target-host, run0 would need a controlling terminal, which we deliberately do not allocate over SSH. Instead the equivalent `systemd-run --uid=0 --pipe --wait ...` form is used. Without --ask-elevate-password this relies on a polkit rule on the target granting the deploying user `org.freedesktop.systemd1.manage-units`. With --ask-elevate-password the command is wrapped in polkit-stdin-agent, which registers a per-process polkit agent for the child and answers the PAM conversation from stdin. A transient unit does not inherit the SSH login environment. On NixOS the unit's PATH is just the systemd store path, so neither `env` nor `nix-env` would be found inside it. Elevator.wrap_remote() therefore now takes the env mapping and the command, and returns the full remote argv: sudo keeps the existing `/bin/sh -c 'exec /usr/bin/env -i ...'` wrapper inside itself, while run0 puts the shell wrapper *around* systemd-run and forwards the resolved values into the unit via --setenv. The Arg/Args/EnvValue/PRESERVE_ENV types and the env-shell helper move from process.py into elevate.py to avoid a circular import, and callers are updated to import them from there. polkit-stdin-agent is resolved on the machine doing the elevation rather than baked in as a host-arch store path, which would be wrong for --no-reexec, cross-arch deploys (re-exec hits ENOEXEC and falls back), --rollback/--store-path, and Darwin deployers. Locally that is PATH. Remotely, Elevator.for_target_config() binds the elevator to the toplevel just placed on the target, and a small /bin/sh picker tries <toplevel>/sw/bin/polkit-stdin-agent (target-arch, in the copied closure) then PATH, exiting with an actionable error pointing at system.tools.nixos-rebuild.enableRun0Elevation when neither resolves. That option (added here) puts the agent in environment.systemPackages and asserts security.polkit.enable. A nixos-rebuild-target-host subtest exercises the full remote run0 path (polkit-stdin-agent + systemd-run + activation) end-to-end. Drop the now-unused stdenv argument from package.nix while here. Closes #507054.
This commit is contained in:
@@ -314,6 +314,27 @@ in
|
||||
name = "nixos-rebuild";
|
||||
package = config.system.build.nixos-rebuild;
|
||||
})
|
||||
(
|
||||
{ config, ... }:
|
||||
{
|
||||
options.system.tools.nixos-rebuild.enableRun0Elevation = lib.mkEnableOption ''
|
||||
support for being targeted by `nixos-rebuild --elevate=run0
|
||||
--ask-elevate-password`.
|
||||
|
||||
This enables polkit and adds {command}`polkit-stdin-agent` to
|
||||
{option}`environment.systemPackages` so that a deploying host
|
||||
can find a target-architecture agent at
|
||||
{file}`<toplevel>/sw/bin/polkit-stdin-agent` after copying the
|
||||
closure (which is required for cross-architecture deploys and
|
||||
mismatched nixpkgs revisions to work).
|
||||
'';
|
||||
|
||||
config = lib.mkIf config.system.tools.nixos-rebuild.enableRun0Elevation {
|
||||
security.polkit.enable = lib.mkDefault true;
|
||||
environment.systemPackages = [ pkgs.polkit-stdin-agent ];
|
||||
};
|
||||
}
|
||||
)
|
||||
(mkToolModule {
|
||||
name = "nixos-version";
|
||||
package = nixos-version;
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
};
|
||||
|
||||
system.includeBuildDependencies = true;
|
||||
# Needed so the offline build of the target config succeeds.
|
||||
system.extraDependencies = [ pkgs.polkit-stdin-agent ];
|
||||
|
||||
virtualisation = {
|
||||
cores = 2;
|
||||
@@ -49,6 +51,11 @@
|
||||
users.users.alice.extraGroups = [ "wheel" ];
|
||||
users.users.bob.extraGroups = [ "wheel" ];
|
||||
|
||||
# Needed for --elevate=run0. NixOS's default polkit admin rule is
|
||||
# `unix-group:wheel`, so bob (in wheel) can authenticate with his
|
||||
# own password via polkit-stdin-agent.
|
||||
system.tools.nixos-rebuild.enableRun0Elevation = true;
|
||||
|
||||
# Disable sudo for root to ensure sudo isn't called without `--sudo`
|
||||
security.sudo.extraRules = lib.mkForce [
|
||||
{
|
||||
@@ -142,6 +149,7 @@
|
||||
deployer.copy_from_host("${configFile "config-1-deployed"}", "/root/configuration-1.nix")
|
||||
deployer.copy_from_host("${configFile "config-2-deployed"}", "/root/configuration-2.nix")
|
||||
deployer.copy_from_host("${configFile "config-3-deployed"}", "/root/configuration-3.nix")
|
||||
deployer.copy_from_host("${configFile "config-4-deployed"}", "/root/configuration-4.nix")
|
||||
deployer.copy_from_host("${targetNetworkJSON}", "/root/target-network.json")
|
||||
deployer.copy_from_host("${targetConfigJSON}", "/root/target-configuration.json")
|
||||
|
||||
@@ -168,6 +176,20 @@
|
||||
target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname").rstrip()
|
||||
assert target_hostname == "config-3-deployed", f"{target_hostname=}"
|
||||
|
||||
with subtest("Deploy to bob@target with run0 and password"):
|
||||
# polkit-stdin-agent registers an agent for systemd-run on the
|
||||
# target and answers the PAM conversation with the password we
|
||||
# supply locally. The agent is resolved on the target from
|
||||
# <toplevel>/sw/bin (see Run0Elevator._remote_agent_argv).
|
||||
deployer.send_chars("nixos-rebuild switch -I nixos-config=/root/configuration-4.nix --target-host bob@target --elevate=run0 --ask-elevate-password\n")
|
||||
deployer.wait_until_tty_matches("1", "\\[run0\\] password for bob@target")
|
||||
deployer.send_chars("${nodes.target.users.users.bob.password}\n")
|
||||
deployer.wait_until_tty_matches("1", "Done. The new configuration is /nix/store/.*config-4-deployed")
|
||||
target_hostname = deployer.succeed("ssh alice@target cat /etc/hostname").rstrip()
|
||||
assert target_hostname == "config-4-deployed", f"{target_hostname=}"
|
||||
# The target-arch agent is reachable at the stable sw/bin path.
|
||||
target.succeed("test -x /run/current-system/sw/bin/polkit-stdin-agent")
|
||||
|
||||
with subtest("Deploy works with very long TMPDIR"):
|
||||
tmp_dir = "/var/folder/veryveryveryveryverylongpathnamethatdoesnotworkwithcontrolpath"
|
||||
deployer.succeed(f"mkdir -p {tmp_dir}")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
callPackage,
|
||||
installShellFiles,
|
||||
mkShell,
|
||||
|
||||
@@ -6,6 +6,12 @@ controlling terminal is available), and how to feed it a pre-supplied
|
||||
password when the backend supports that. ``run_wrapper`` and its callers
|
||||
carry a single ``elevate: Elevator`` value and let it produce the command
|
||||
prefix and stdin.
|
||||
|
||||
The remote case has no controlling terminal and the elevated command's
|
||||
environment depends on the backend (``sudo`` inherits the SSH login env,
|
||||
while the run0 backend starts a transient unit with only systemd's
|
||||
default ``PATH``), so each backend builds the full remote argv itself
|
||||
via :meth:`Elevator.wrap_remote`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -14,24 +20,82 @@ import getpass
|
||||
import os
|
||||
import shlex
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field, replace
|
||||
from enum import Enum
|
||||
from typing import Final, Self, override
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import ClassVar, Final, Literal, Self, override
|
||||
|
||||
# Kept here (rather than in process.py) so that elevators can build remote
|
||||
# argvs without a circular import.
|
||||
type Arg = str | bytes | os.PathLike[str] | os.PathLike[bytes]
|
||||
type Args = Sequence[Arg]
|
||||
|
||||
|
||||
class _Env(Enum):
|
||||
PRESERVE_ENV = "PRESERVE"
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
#: Sentinel meaning "copy this variable from the environment the wrapped
|
||||
#: command would naturally see" (``os.environ`` locally, the SSH login
|
||||
#: shell's environment remotely).
|
||||
PRESERVE_ENV: Final = _Env.PRESERVE_ENV
|
||||
|
||||
type EnvValue = str | Literal[_Env.PRESERVE_ENV]
|
||||
|
||||
|
||||
def _remote_env_shell_argv(
|
||||
prefix: Sequence[str],
|
||||
env: Mapping[str, EnvValue],
|
||||
args: Args,
|
||||
) -> list[Arg]:
|
||||
"""Build ``<prefix> /bin/sh -c 'exec /usr/bin/env -i K=V… "$@"' sh <args>``.
|
||||
|
||||
The wrapper runs in the SSH login session, resolves ``PRESERVE_ENV``
|
||||
variables against that session's environment, and re-execs the command
|
||||
with exactly that set. ``/usr/bin/env`` is referenced by absolute path so
|
||||
the wrapper does not depend on ``PATH`` itself (provided on NixOS via
|
||||
``environment.usrbinenv``).
|
||||
"""
|
||||
assigns: list[str] = []
|
||||
for k, v in env.items():
|
||||
if v is PRESERVE_ENV:
|
||||
assigns.append(f'{k}="${{{k}-}}"')
|
||||
else:
|
||||
assigns.append(f"{k}={shlex.quote(v)}")
|
||||
script = f'exec /usr/bin/env -i {" ".join(assigns)} "$@"'
|
||||
return [*prefix, "/bin/sh", "-c", script, "sh", *args]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Wrapped:
|
||||
"""Result of wrapping a command for elevation."""
|
||||
"""Result of wrapping a command for local elevation."""
|
||||
|
||||
#: Arguments to prepend to the command. Kept as ``list[str]`` rather than
|
||||
#: the wider ``process.Args`` because elevators only ever prepend plain
|
||||
#: strings; callers splice this into their existing ``Args`` list.
|
||||
#: Arguments to prepend to the command.
|
||||
prefix: list[str]
|
||||
#: Text to send on the wrapped command's stdin (typically a password
|
||||
#: followed by a newline), or ``None`` to leave stdin alone.
|
||||
stdin: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RemoteWrapped:
|
||||
"""Result of wrapping a command for remote elevation over SSH.
|
||||
|
||||
Unlike :class:`Wrapped` this carries the *full* remote argv: backends
|
||||
differ in where the env-resolution shell wrapper must sit relative to
|
||||
the elevator (inside ``sudo``, but *around* ``systemd-run``), so a
|
||||
plain prefix is not expressive enough.
|
||||
"""
|
||||
|
||||
argv: list[Arg]
|
||||
stdin: str | None = None
|
||||
|
||||
|
||||
class Elevator(ABC):
|
||||
"""How to gain root for activation commands."""
|
||||
|
||||
@@ -54,13 +118,18 @@ class Elevator(ABC):
|
||||
"""Wrap a command run on the local machine."""
|
||||
|
||||
@abstractmethod
|
||||
def wrap_remote(self) -> Wrapped:
|
||||
def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped:
|
||||
"""Wrap a command run on a target host over SSH.
|
||||
|
||||
The remote side has no controlling terminal, so backends that need
|
||||
interactive prompts must either accept a pre-supplied password
|
||||
(see :meth:`with_password`) or rely on a passwordless policy on
|
||||
the target.
|
||||
|
||||
*env* is the environment to establish for the elevated command.
|
||||
:data:`PRESERVE_ENV` values are resolved against the SSH login
|
||||
shell's environment, and the backend must do so before any step
|
||||
that replaces it with a service-style one.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -83,6 +152,17 @@ class Elevator(ABC):
|
||||
password = getpass.getpass(f"[{self.name}] password for {host_label}: ")
|
||||
return self.with_password(password)
|
||||
|
||||
def for_target_config(self, toplevel: PurePosixPath | Path) -> Self:
|
||||
"""Return a copy bound to the toplevel being activated on the target.
|
||||
|
||||
Backends that need a helper binary on the remote
|
||||
(:class:`Run0Elevator`'s ``polkit-stdin-agent``) use this to find
|
||||
a target-architecture copy inside the just-copied closure. No-op
|
||||
by default.
|
||||
"""
|
||||
del toplevel # unused in the base implementation
|
||||
return self
|
||||
|
||||
def on_remote_failure(self) -> str | None:
|
||||
"""Optional hint to print when a remote elevated command fails."""
|
||||
return None
|
||||
@@ -106,8 +186,8 @@ class NoElevator(Elevator):
|
||||
return Wrapped(prefix=[])
|
||||
|
||||
@override
|
||||
def wrap_remote(self) -> Wrapped:
|
||||
return Wrapped(prefix=[])
|
||||
def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped:
|
||||
return RemoteWrapped(argv=_remote_env_shell_argv([], env, args))
|
||||
|
||||
@override
|
||||
def with_password(self, password: str) -> Self:
|
||||
@@ -134,13 +214,6 @@ class SudoElevator(Elevator):
|
||||
def wrap_local(self) -> Wrapped:
|
||||
# Local sudo can prompt on /dev/tty itself, so the password is
|
||||
# only piped when one was supplied explicitly.
|
||||
return self._wrap()
|
||||
|
||||
@override
|
||||
def wrap_remote(self) -> Wrapped:
|
||||
return self._wrap()
|
||||
|
||||
def _wrap(self) -> Wrapped:
|
||||
if self.password is not None:
|
||||
return Wrapped(
|
||||
prefix=["sudo", "--prompt=", "--stdin", *self.extra_opts],
|
||||
@@ -148,6 +221,21 @@ class SudoElevator(Elevator):
|
||||
)
|
||||
return Wrapped(prefix=["sudo", *self.extra_opts])
|
||||
|
||||
@override
|
||||
def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped:
|
||||
# sudo runs inside the SSH login session, so the env wrapper can
|
||||
# sit *inside* it and ${VAR-} resolves against the login env.
|
||||
if self.password is not None:
|
||||
prefix = ["sudo", "--prompt=", "--stdin", *self.extra_opts]
|
||||
stdin = self.password + "\n"
|
||||
else:
|
||||
prefix = ["sudo", *self.extra_opts]
|
||||
stdin = None
|
||||
return RemoteWrapped(
|
||||
argv=_remote_env_shell_argv(prefix, env, args),
|
||||
stdin=stdin,
|
||||
)
|
||||
|
||||
@override
|
||||
def with_password(self, password: str) -> Self:
|
||||
return replace(self, password=password)
|
||||
@@ -162,16 +250,144 @@ class SudoElevator(Elevator):
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Run0Elevator(Elevator):
|
||||
"""Wrap with systemd's polkit-based ``run0``.
|
||||
|
||||
Locally, ``run0`` is used directly and the user's polkit agent
|
||||
(graphical or ``pkttyagent``) handles any prompts.
|
||||
|
||||
Remotely we spell out the explicit ``systemd-run --uid=0 --pipe``
|
||||
form instead. ``run0`` would internally do the same thing when stdio
|
||||
is not a TTY (see systemd ``src/run/run.c``), but going through
|
||||
``systemd-run`` directly gives us ``--setenv K=V``, which we need to
|
||||
forward the SSH login environment into the transient unit (whose own
|
||||
``PATH`` on NixOS is just the systemd store path), and keeps the
|
||||
argv independent of whether the SSH session happens to have a TTY.
|
||||
|
||||
Authorisation comes from either a polkit rule on the target granting
|
||||
``org.freedesktop.systemd1.manage-units`` to the deploying user, or
|
||||
from ``polkit-stdin-agent`` for ``--ask-elevate-password``.
|
||||
"""
|
||||
|
||||
name: str = "run0"
|
||||
password: str | None = None
|
||||
#: ``${toplevel}/sw/bin/polkit-stdin-agent`` on the target, set via
|
||||
#: :meth:`for_target_config`. ``None`` falls back to the target's ``PATH``.
|
||||
remote_agent: str | None = None
|
||||
|
||||
#: Non-interactive equivalent of ``run0`` (see the class docstring).
|
||||
REMOTE_BASE: ClassVar[tuple[str, ...]] = (
|
||||
"systemd-run",
|
||||
"--uid=0",
|
||||
"--pipe",
|
||||
"--quiet",
|
||||
"--wait",
|
||||
"--collect",
|
||||
"--service-type=exec",
|
||||
"--send-sighup",
|
||||
)
|
||||
|
||||
@override
|
||||
def wrap_local(self) -> Wrapped:
|
||||
if self.password is not None:
|
||||
# Resolved from PATH, same requirement as the remote case: the
|
||||
# machine doing the elevation needs
|
||||
# system.tools.nixos-rebuild.enableRun0Elevation.
|
||||
return Wrapped(
|
||||
prefix=["polkit-stdin-agent", "--password-fd=0", "--", "run0", "--"],
|
||||
stdin=self.password + "\n",
|
||||
)
|
||||
return Wrapped(prefix=["run0", "--"])
|
||||
|
||||
@override
|
||||
def wrap_remote(self, env: Mapping[str, EnvValue], args: Args) -> RemoteWrapped:
|
||||
# /bin/sh wrapper resolves PRESERVE_ENV in the SSH login session
|
||||
# and forwards the result into the unit via --setenv.
|
||||
setenvs: list[str] = []
|
||||
for k, v in env.items():
|
||||
if v is PRESERVE_ENV:
|
||||
setenvs.append(f'--setenv={k}="${{{k}-}}"')
|
||||
else:
|
||||
setenvs.append(f"--setenv={shlex.quote(f'{k}={v}')}")
|
||||
script = f'exec {shlex.join(self.REMOTE_BASE)} {" ".join(setenvs)} -- "$@"'
|
||||
argv: list[Arg] = ["/bin/sh", "-c", script, "sh", *args]
|
||||
if self.password is not None:
|
||||
# polkit has no `sudo --stdin` equivalent. polkit-stdin-agent
|
||||
# registers a per-process agent for the wrapped command and
|
||||
# answers the PAM conversation from its stdin.
|
||||
argv = self._remote_agent_argv(argv)
|
||||
return RemoteWrapped(argv=argv, stdin=self.password + "\n")
|
||||
return RemoteWrapped(argv=argv)
|
||||
|
||||
#: POSIX sh fragment that picks the first runnable agent from the
|
||||
#: positional parameters up to ``--`` and execs it with the remainder.
|
||||
#: ``command -v`` covers both absolute paths and ``PATH`` lookups.
|
||||
_AGENT_PICKER: ClassVar[str] = (
|
||||
"agent=; "
|
||||
"for a; do "
|
||||
"shift; "
|
||||
'[ "$a" = -- ] && break; '
|
||||
'[ -z "$agent" ] && command -v "$a" >/dev/null 2>&1 && agent="$a"; '
|
||||
"done; "
|
||||
'[ -n "$agent" ] && exec "$agent" --password-fd=0 -- "$@"; '
|
||||
'echo "nixos-rebuild: polkit-stdin-agent not found on target host '
|
||||
'(set system.tools.nixos-rebuild.enableRun0Elevation = true)" >&2; '
|
||||
"exit 127"
|
||||
)
|
||||
|
||||
def _remote_agent_argv(self, inner: list[Arg]) -> list[Arg]:
|
||||
"""Wrap *inner* in a target-side agent lookup.
|
||||
|
||||
The deployer's own agent may be the wrong arch/nixpkgs (cross-arch
|
||||
deploys, Darwin deployers, ``--no-reexec``), so resolve on the
|
||||
target instead: first ``${toplevel}/sw/bin/polkit-stdin-agent``
|
||||
(present when ``system.tools.nixos-rebuild.enableRun0Elevation``
|
||||
is set), then bare ``polkit-stdin-agent`` on the SSH login PATH.
|
||||
:data:`_AGENT_PICKER` exits 127 with a hint if neither is found.
|
||||
"""
|
||||
candidates: list[str] = []
|
||||
if self.remote_agent is not None:
|
||||
candidates.append(self.remote_agent)
|
||||
candidates.append("polkit-stdin-agent")
|
||||
return ["/bin/sh", "-c", self._AGENT_PICKER, "sh", *candidates, "--", *inner]
|
||||
|
||||
@override
|
||||
def with_password(self, password: str) -> Self:
|
||||
return replace(self, password=password)
|
||||
|
||||
@override
|
||||
def for_target_config(self, toplevel: PurePosixPath | Path) -> Self:
|
||||
return replace(
|
||||
self, remote_agent=str(toplevel / "sw" / "bin" / "polkit-stdin-agent")
|
||||
)
|
||||
|
||||
@override
|
||||
def on_remote_failure(self) -> str | None:
|
||||
if self.password is None:
|
||||
return (
|
||||
"while running command with remote run0. Either pass "
|
||||
"--ask-elevate-password, or grant the deploying user the "
|
||||
"polkit action 'org.freedesktop.systemd1.manage-units' on "
|
||||
"the target host (security.polkit.extraConfig)."
|
||||
)
|
||||
return (
|
||||
"while running command with remote run0. If the error above "
|
||||
"mentions polkit-stdin-agent or PolicyKit1, the target host "
|
||||
"needs system.tools.nixos-rebuild.enableRun0Elevation = true."
|
||||
)
|
||||
|
||||
|
||||
class ElevatorKind(Enum):
|
||||
"""CLI-selectable elevation backends.
|
||||
|
||||
The enum *value* is the :class:`Elevator` subclass to instantiate,
|
||||
``str(member)`` is what ``--elevate`` accepts on the command line.
|
||||
Extended by later commits.
|
||||
"""
|
||||
|
||||
NONE = NoElevator
|
||||
SUDO = SudoElevator
|
||||
RUN0 = Run0Elevator
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -14,7 +14,7 @@ from textwrap import dedent
|
||||
from typing import Final, Literal
|
||||
|
||||
from . import tmpdir
|
||||
from .elevate import NO_ELEVATOR, Elevator
|
||||
from .elevate import NO_ELEVATOR, PRESERVE_ENV, Elevator
|
||||
from .models import (
|
||||
Action,
|
||||
BuildAttr,
|
||||
@@ -26,7 +26,7 @@ from .models import (
|
||||
Profile,
|
||||
Remote,
|
||||
)
|
||||
from .process import PRESERVE_ENV, SSH_DEFAULT_OPTS, run_wrapper
|
||||
from .process import SSH_DEFAULT_OPTS, run_wrapper
|
||||
from .utils import Args, dict_to_flags
|
||||
|
||||
FLAKE_FLAGS: Final = ["--extra-experimental-features", "nix-command flakes"]
|
||||
|
||||
@@ -4,14 +4,20 @@ import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from ipaddress import AddressValueError, IPv6Address
|
||||
from typing import Final, Literal, Self, TextIO, TypedDict, Unpack, override
|
||||
from typing import Final, Self, TextIO, TypedDict, Unpack
|
||||
|
||||
from . import tmpdir
|
||||
from .elevate import NO_ELEVATOR, Elevator
|
||||
from .elevate import (
|
||||
NO_ELEVATOR,
|
||||
PRESERVE_ENV,
|
||||
Arg,
|
||||
Args,
|
||||
Elevator,
|
||||
EnvValue,
|
||||
)
|
||||
|
||||
logger: Final = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,22 +31,6 @@ SSH_DEFAULT_OPTS: Final = [
|
||||
]
|
||||
|
||||
|
||||
class _Env(Enum):
|
||||
PRESERVE_ENV = "PRESERVE"
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
PRESERVE_ENV: Final = _Env.PRESERVE_ENV
|
||||
|
||||
|
||||
type Arg = str | bytes | os.PathLike[str] | os.PathLike[bytes]
|
||||
type Args = Sequence[Arg]
|
||||
type EnvValue = str | Literal[_Env.PRESERVE_ENV]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Remote:
|
||||
host: str
|
||||
@@ -136,16 +126,9 @@ def run_wrapper(
|
||||
resolved_env = _resolve_env_local(normalized_env)
|
||||
|
||||
if remote:
|
||||
wrapped = elevate.wrap_remote()
|
||||
process_input = wrapped.stdin
|
||||
remote_run_args: list[Arg] = [
|
||||
*wrapped.prefix,
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
_remote_shell_script(normalized_env),
|
||||
"sh",
|
||||
*run_args,
|
||||
]
|
||||
rwrapped = elevate.wrap_remote(normalized_env, run_args)
|
||||
process_input = rwrapped.stdin
|
||||
remote_run_args: list[Arg] = rwrapped.argv
|
||||
|
||||
ssh_args: list[Arg] = [
|
||||
"ssh",
|
||||
@@ -246,7 +229,7 @@ def _resolve_env_local(env: dict[str, EnvValue]) -> dict[str, str]:
|
||||
return result
|
||||
|
||||
|
||||
def _prefix_env_cmd(cmd: Sequence[Arg], resolved_env: dict[str, str]) -> list[Arg]:
|
||||
def _prefix_env_cmd(cmd: Args, resolved_env: dict[str, str]) -> list[Arg]:
|
||||
"""
|
||||
Prefix a command with `env -i K=V ... -- <cmd...>` to set vars for the
|
||||
command.
|
||||
@@ -258,24 +241,6 @@ def _prefix_env_cmd(cmd: Sequence[Arg], resolved_env: dict[str, str]) -> list[Ar
|
||||
return ["env", "-i", *assigns, *cmd]
|
||||
|
||||
|
||||
def _remote_shell_script(env: Mapping[str, EnvValue]) -> str:
|
||||
"""
|
||||
Build the POSIX shell wrapper used for remote execution over SSH.
|
||||
|
||||
SSH sends the remote command as a shell-interpreted command line, so we
|
||||
need a wrapper to establish a clean environment before `exec`-ing the real
|
||||
command. This wrapper is always run under `/bin/sh -c` so preserved
|
||||
variables like `${PATH-}` do not depend on the remote user's login shell.
|
||||
"""
|
||||
shell_assigns: list[str] = []
|
||||
for k, v in env.items():
|
||||
if v is PRESERVE_ENV:
|
||||
shell_assigns.append(f'{k}="${{{k}-}}"')
|
||||
else:
|
||||
shell_assigns.append(f"{k}={shlex.quote(v)}")
|
||||
return f'exec env -i {" ".join(shell_assigns)} "$@"'
|
||||
|
||||
|
||||
def _quote_remote_arg(arg: Arg) -> str:
|
||||
return shlex.quote(str(arg))
|
||||
|
||||
|
||||
@@ -298,6 +298,11 @@ def build_and_activate_system(
|
||||
copy_flags=grouped_nix_args.copy_flags,
|
||||
)
|
||||
elif args.rollback:
|
||||
if target_host is not None:
|
||||
# The elevated `nix-env --rollback` runs before path_to_config
|
||||
# is known, so point the elevator at the profile to find a
|
||||
# target-arch helper in the *current* generation's sw/bin.
|
||||
args.elevator = args.elevator.for_target_config(profile.path)
|
||||
path_to_config = _rollback_system(
|
||||
action=action,
|
||||
args=args,
|
||||
@@ -315,6 +320,11 @@ def build_and_activate_system(
|
||||
grouped_nix_args=grouped_nix_args,
|
||||
)
|
||||
|
||||
if target_host is not None and not args.rollback:
|
||||
# Prefer the helper from the toplevel we just copied to the
|
||||
# target (correct arch, independent of re-exec / nixpkgs pin).
|
||||
args.elevator = args.elevator.for_target_config(path_to_config)
|
||||
|
||||
current_config = Path("/run/current-system")
|
||||
if args.diff:
|
||||
if current_config.exists():
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
import pytest
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
@@ -8,7 +10,10 @@ def test_no_elevator() -> None:
|
||||
n = e.NoElevator()
|
||||
assert not n.elevates
|
||||
assert n.wrap_local() == e.Wrapped(prefix=[])
|
||||
assert n.wrap_remote() == e.Wrapped(prefix=[])
|
||||
rw = n.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
assert rw.stdin is None
|
||||
assert rw.argv[:2] == ["/bin/sh", "-c"]
|
||||
assert rw.argv[-1] == "cmd"
|
||||
with pytest.raises(e.ElevateError):
|
||||
n.with_password("x")
|
||||
|
||||
@@ -19,14 +24,16 @@ def test_sudo_elevator(monkeypatch: MonkeyPatch) -> None:
|
||||
s = e.SudoElevator()
|
||||
assert s.elevates
|
||||
assert s.wrap_local() == e.Wrapped(prefix=["sudo"])
|
||||
assert s.wrap_remote() == e.Wrapped(prefix=["sudo"])
|
||||
rw = s.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
assert rw.argv[0] == "sudo"
|
||||
assert rw.argv[1:3] == ["/bin/sh", "-c"]
|
||||
assert rw.stdin is None
|
||||
assert s.on_remote_failure() is not None
|
||||
|
||||
sp = s.with_password("hunter2")
|
||||
assert sp.wrap_remote() == e.Wrapped(
|
||||
prefix=["sudo", "--prompt=", "--stdin"],
|
||||
stdin="hunter2\n",
|
||||
)
|
||||
rw = sp.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
assert rw.argv[:3] == ["sudo", "--prompt=", "--stdin"]
|
||||
assert rw.stdin == "hunter2\n"
|
||||
assert sp.on_remote_failure() is None
|
||||
# original unchanged
|
||||
assert s.password is None
|
||||
@@ -36,18 +43,94 @@ def test_sudo_elevator_extra_opts(monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("NIX_SUDOOPTS", "--preserve-env=FOO -H")
|
||||
s = e.SudoElevator()
|
||||
assert s.wrap_local() == e.Wrapped(prefix=["sudo", "--preserve-env=FOO", "-H"])
|
||||
assert s.with_password("p").wrap_remote() == e.Wrapped(
|
||||
prefix=["sudo", "--prompt=", "--stdin", "--preserve-env=FOO", "-H"],
|
||||
stdin="p\n",
|
||||
rw = s.with_password("p").wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
assert rw.argv[:5] == ["sudo", "--prompt=", "--stdin", "--preserve-env=FOO", "-H"]
|
||||
assert rw.stdin == "p\n"
|
||||
|
||||
|
||||
def test_run0_elevator() -> None:
|
||||
r = e.Run0Elevator()
|
||||
assert r.elevates
|
||||
assert r.wrap_local() == e.Wrapped(prefix=["run0", "--"])
|
||||
assert r.on_remote_failure() is not None
|
||||
|
||||
rp = r.with_password("hunter2")
|
||||
w = rp.wrap_local()
|
||||
assert w.prefix[0] == "polkit-stdin-agent"
|
||||
assert "run0" in w.prefix
|
||||
assert w.stdin == "hunter2\n"
|
||||
# With a password the failure hint points at the agent, not at -S.
|
||||
hint = rp.on_remote_failure()
|
||||
assert hint is not None and "polkit-stdin-agent" in hint
|
||||
|
||||
|
||||
def test_run0_elevator_remote() -> None:
|
||||
r = e.Run0Elevator()
|
||||
|
||||
# No password: /bin/sh wrapper around systemd-run, env passed via
|
||||
# --setenv so it is resolved in the SSH login shell rather than
|
||||
# inside the transient unit (which has a useless PATH on NixOS).
|
||||
rw = r.wrap_remote(
|
||||
{"PATH": e.PRESERVE_ENV, "NIXOS_INSTALL_BOOTLOADER": "1"},
|
||||
["nix-env", "-p", "/profile"],
|
||||
)
|
||||
assert rw.stdin is None
|
||||
assert rw.argv[:2] == ["/bin/sh", "-c"]
|
||||
script = rw.argv[2]
|
||||
assert isinstance(script, str)
|
||||
assert script.startswith("exec systemd-run --uid=0 --pipe ")
|
||||
assert '--setenv=PATH="${PATH-}"' in script
|
||||
assert "--setenv=NIXOS_INSTALL_BOOTLOADER=1" in script
|
||||
assert script.endswith(' -- "$@"')
|
||||
assert rw.argv[3:] == ["sh", "nix-env", "-p", "/profile"]
|
||||
|
||||
# With password: an agent-picker /bin/sh wraps the inner /bin/sh, so
|
||||
# the agent is registered for the inner shell (and the systemd-run it
|
||||
# execs into). With no toplevel bound the only candidate is bare-name
|
||||
# PATH lookup.
|
||||
rw = r.with_password("pw").wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
assert rw.stdin == "pw\n"
|
||||
assert rw.argv[:3] == ["/bin/sh", "-c", e.Run0Elevator._AGENT_PICKER]
|
||||
sep = rw.argv.index("--")
|
||||
assert rw.argv[3:sep] == ["sh", "polkit-stdin-agent"]
|
||||
assert rw.argv[sep + 1 : sep + 3] == ["/bin/sh", "-c"]
|
||||
|
||||
# Explicit values containing spaces are shell-quoted inside the
|
||||
# script (the whole thing is later shlex.quoted again for SSH).
|
||||
rw = r.wrap_remote({"FOO": "a b"}, ["cmd"])
|
||||
script = rw.argv[2]
|
||||
assert isinstance(script, str)
|
||||
assert "--setenv='FOO=a b'" in script
|
||||
|
||||
|
||||
def test_run0_for_target_config() -> None:
|
||||
toplevel = PurePosixPath("/nix/store/aaaa-nixos-system")
|
||||
r = e.Run0Elevator().with_password("pw").for_target_config(toplevel)
|
||||
assert r.remote_agent == f"{toplevel}/sw/bin/polkit-stdin-agent"
|
||||
|
||||
rw = r.wrap_remote({"PATH": e.PRESERVE_ENV}, ["cmd"])
|
||||
sep = rw.argv.index("--")
|
||||
# Order matters: target-arch toplevel first, then PATH.
|
||||
assert rw.argv[4:sep] == [
|
||||
f"{toplevel}/sw/bin/polkit-stdin-agent",
|
||||
"polkit-stdin-agent",
|
||||
]
|
||||
# Inner argv is preserved verbatim after the separator.
|
||||
assert rw.argv[sep + 1 : sep + 3] == ["/bin/sh", "-c"]
|
||||
assert rw.argv[-1] == "cmd"
|
||||
|
||||
# Non-run0 elevators ignore the toplevel.
|
||||
s = e.SudoElevator()
|
||||
assert s.for_target_config(toplevel) is s
|
||||
|
||||
|
||||
def test_elevator_kind() -> None:
|
||||
assert isinstance(e.ElevatorKind.from_name("sudo"), e.SudoElevator)
|
||||
assert isinstance(e.ElevatorKind.from_name("run0"), e.Run0Elevator)
|
||||
assert isinstance(e.ElevatorKind.from_name("none"), e.NoElevator)
|
||||
with pytest.raises(e.ElevateError):
|
||||
e.ElevatorKind.from_name("doas")
|
||||
assert set(e.ElevatorKind.choices()) == {"none", "sudo"}
|
||||
assert set(e.ElevatorKind.choices()) == {"none", "sudo", "run0"}
|
||||
|
||||
|
||||
def test_with_prompted_password(monkeypatch: MonkeyPatch) -> None:
|
||||
|
||||
@@ -28,6 +28,9 @@ def test_parse_args_elevate() -> None:
|
||||
r, _ = nr.parse_args(["nixos-rebuild", "switch", "--elevate=sudo"])
|
||||
assert isinstance(r.elevator, nr.elevate.SudoElevator)
|
||||
|
||||
r, _ = nr.parse_args(["nixos-rebuild", "switch", "--elevate=run0"])
|
||||
assert isinstance(r.elevator, nr.elevate.Run0Elevator)
|
||||
|
||||
# back-compat aliases
|
||||
for flag in ("--sudo", "--use-remote-sudo"):
|
||||
r, _ = nr.parse_args(["nixos-rebuild", "switch", flag])
|
||||
@@ -691,7 +694,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"mktemp",
|
||||
"-d",
|
||||
@@ -710,7 +713,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix-store",
|
||||
"--realise",
|
||||
@@ -730,7 +733,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"readlink",
|
||||
"-f",
|
||||
@@ -748,7 +751,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"rm",
|
||||
"-rf",
|
||||
@@ -781,7 +784,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix-env",
|
||||
"-p",
|
||||
@@ -800,7 +803,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"-d",
|
||||
@@ -818,7 +821,7 @@ def test_execute_nix_switch_build_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"sh",
|
||||
*nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX,
|
||||
str(config_path / "bin/switch-to-configuration"),
|
||||
@@ -907,7 +910,7 @@ def test_execute_nix_switch_flake_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix-env",
|
||||
"-p",
|
||||
@@ -926,7 +929,7 @@ def test_execute_nix_switch_flake_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"-d",
|
||||
@@ -944,7 +947,7 @@ def test_execute_nix_switch_flake_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"sh",
|
||||
*nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX,
|
||||
str(config_path / "bin/switch-to-configuration"),
|
||||
@@ -1032,7 +1035,7 @@ def test_execute_nix_switch_flake_build_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
@@ -1271,7 +1274,7 @@ def test_execute_build_dry_run_build_and_target_remote(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix",
|
||||
"--extra-experimental-features",
|
||||
@@ -1526,7 +1529,7 @@ def test_execute_switch_store_path_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"-f",
|
||||
@@ -1544,7 +1547,7 @@ def test_execute_switch_store_path_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"nix-env",
|
||||
"-p",
|
||||
@@ -1563,7 +1566,7 @@ def test_execute_switch_store_path_target_host(
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"-d",
|
||||
@@ -1581,7 +1584,7 @@ def test_execute_switch_store_path_target_host(
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'""",
|
||||
"sh",
|
||||
*nr.nix.SWITCH_TO_CONFIGURATION_CMD_PREFIX,
|
||||
str(config_path / "bin/switch-to-configuration"),
|
||||
|
||||
@@ -767,8 +767,8 @@ def test_switch_to_configuration_without_systemd_run(
|
||||
mock_run.assert_called_with(
|
||||
[profile_path / "bin/switch-to-configuration", "switch"],
|
||||
env={
|
||||
"LOCALE_ARCHIVE": p.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": p.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": e.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": e.PRESERVE_ENV,
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
},
|
||||
elevate=e.NO_ELEVATOR,
|
||||
@@ -809,8 +809,8 @@ def test_switch_to_configuration_without_systemd_run(
|
||||
"test",
|
||||
],
|
||||
env={
|
||||
"LOCALE_ARCHIVE": p.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": p.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": e.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": e.PRESERVE_ENV,
|
||||
"NIXOS_INSTALL_BOOTLOADER": "1",
|
||||
},
|
||||
elevate=SUDO,
|
||||
@@ -845,8 +845,8 @@ def test_switch_to_configuration_with_systemd_run(
|
||||
"switch",
|
||||
],
|
||||
env={
|
||||
"LOCALE_ARCHIVE": p.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": p.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": e.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": e.PRESERVE_ENV,
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
},
|
||||
elevate=e.NO_ELEVATOR,
|
||||
@@ -875,8 +875,8 @@ def test_switch_to_configuration_with_systemd_run(
|
||||
"test",
|
||||
],
|
||||
env={
|
||||
"LOCALE_ARCHIVE": p.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": p.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": e.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": e.PRESERVE_ENV,
|
||||
"NIXOS_INSTALL_BOOTLOADER": "1",
|
||||
},
|
||||
elevate=SUDO,
|
||||
|
||||
@@ -8,24 +8,41 @@ import nixos_rebuild.models as m
|
||||
import nixos_rebuild.process as p
|
||||
|
||||
|
||||
def test_remote_shell_script() -> None:
|
||||
assert p._remote_shell_script({"PATH": p.PRESERVE_ENV}) == (
|
||||
'''exec env -i PATH="${PATH-}" "$@"'''
|
||||
)
|
||||
assert p._remote_shell_script(
|
||||
def test_remote_env_shell_argv() -> None:
|
||||
assert e._remote_env_shell_argv([], {"PATH": e.PRESERVE_ENV}, ["cmd"]) == [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
'''exec /usr/bin/env -i PATH="${PATH-}" "$@"''',
|
||||
"sh",
|
||||
"cmd",
|
||||
]
|
||||
assert e._remote_env_shell_argv(
|
||||
["sudo"],
|
||||
{
|
||||
"PATH": p.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": p.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": p.PRESERVE_ENV,
|
||||
"PATH": e.PRESERVE_ENV,
|
||||
"LOCALE_ARCHIVE": e.PRESERVE_ENV,
|
||||
"NIXOS_NO_CHECK": e.PRESERVE_ENV,
|
||||
"NIXOS_INSTALL_BOOTLOADER": "0",
|
||||
}
|
||||
) == (
|
||||
"""exec env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" """
|
||||
'''NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"'''
|
||||
)
|
||||
assert p._remote_shell_script({"PATH": p.PRESERVE_ENV, "FOO": "some value"}) == (
|
||||
'''exec env -i PATH="${PATH-}" FOO='some value' "$@"'''
|
||||
)
|
||||
},
|
||||
["cmd", "arg"],
|
||||
) == [
|
||||
"sudo",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" """
|
||||
'''NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"''',
|
||||
"sh",
|
||||
"cmd",
|
||||
"arg",
|
||||
]
|
||||
assert e._remote_env_shell_argv(
|
||||
[], {"PATH": e.PRESERVE_ENV, "FOO": "some value"}, []
|
||||
) == [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
'''exec /usr/bin/env -i PATH="${PATH-}" FOO='some value' "$@"''',
|
||||
"sh",
|
||||
]
|
||||
|
||||
|
||||
@patch.dict(p.os.environ, {"PATH": "/path/to/bin"}, clear=True)
|
||||
@@ -123,7 +140,7 @@ def test_run_wrapper(mock_run: Any) -> None:
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"--with",
|
||||
@@ -156,7 +173,7 @@ def test_run_wrapper(mock_run: Any) -> None:
|
||||
"--stdin",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" FOO=bar "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" FOO=bar "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
"--with",
|
||||
@@ -248,6 +265,82 @@ def test_ssh_host() -> None:
|
||||
assert remote.store_type == "ssh-ng"
|
||||
|
||||
|
||||
@patch.dict(p.os.environ, {"PATH": "/path/to/bin"}, clear=True)
|
||||
@patch("subprocess.run", autospec=True)
|
||||
def test_run_wrapper_run0(mock_run: Any) -> None:
|
||||
p.run_wrapper(["cmd", "arg"], elevate=e.Run0Elevator())
|
||||
mock_run.assert_called_with(
|
||||
["run0", "--", "cmd", "arg"],
|
||||
check=True,
|
||||
text=True,
|
||||
errors="surrogateescape",
|
||||
env=None,
|
||||
input=None,
|
||||
)
|
||||
|
||||
run0_script = (
|
||||
"exec systemd-run --uid=0 --pipe --quiet --wait --collect "
|
||||
"--service-type=exec --send-sighup "
|
||||
'--setenv=PATH="${PATH-}" -- "$@"'
|
||||
)
|
||||
|
||||
p.run_wrapper(
|
||||
["cmd", "arg"],
|
||||
elevate=e.Run0Elevator(),
|
||||
remote=m.Remote("user@host", [], "ssh"),
|
||||
)
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"ssh",
|
||||
*p.SSH_DEFAULT_OPTS,
|
||||
"user@host",
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
p._quote_remote_arg(run0_script),
|
||||
"sh",
|
||||
"cmd",
|
||||
"arg",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
errors="surrogateescape",
|
||||
env=None,
|
||||
input=None,
|
||||
)
|
||||
|
||||
p.run_wrapper(
|
||||
["cmd"],
|
||||
elevate=e.Run0Elevator().with_password("pw"),
|
||||
remote=m.Remote("user@host", [], "ssh"),
|
||||
)
|
||||
mock_run.assert_called_with(
|
||||
[
|
||||
"ssh",
|
||||
*p.SSH_DEFAULT_OPTS,
|
||||
"user@host",
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
p._quote_remote_arg(e.Run0Elevator._AGENT_PICKER),
|
||||
"sh",
|
||||
# No toplevel bound, so the only candidate is PATH lookup.
|
||||
"polkit-stdin-agent",
|
||||
"--",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
p._quote_remote_arg(run0_script),
|
||||
"sh",
|
||||
"cmd",
|
||||
],
|
||||
check=True,
|
||||
text=True,
|
||||
errors="surrogateescape",
|
||||
env=None,
|
||||
input="pw\n",
|
||||
)
|
||||
|
||||
|
||||
@patch("subprocess.run", autospec=True)
|
||||
def test_custom_sudo_args(mock_run: Any) -> None:
|
||||
with patch.dict(
|
||||
@@ -298,7 +391,7 @@ def test_custom_sudo_args(mock_run: Any) -> None:
|
||||
"--args",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"""'exec env -i PATH="${PATH-}" "$@"'""",
|
||||
"""'exec /usr/bin/env -i PATH="${PATH-}" "$@"'""",
|
||||
"sh",
|
||||
"test",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user