Merge pull request 'clan_cli: fix support for non-root deployment user (from rtunreal)' (#3124) from Qubasa/clan-core:main into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3124
This commit is contained in:
@@ -165,7 +165,6 @@
|
|||||||
(modulesPath + "/../tests/common/auto-format-root-device.nix")
|
(modulesPath + "/../tests/common/auto-format-root-device.nix")
|
||||||
];
|
];
|
||||||
services.openssh.enable = true;
|
services.openssh.enable = true;
|
||||||
users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
|
||||||
system.nixos.variant_id = "installer";
|
system.nixos.variant_id = "installer";
|
||||||
environment.systemPackages = [ pkgs.nixos-facter ];
|
environment.systemPackages = [ pkgs.nixos-facter ];
|
||||||
virtualisation.emptyDiskImages = [ 512 ];
|
virtualisation.emptyDiskImages = [ 512 ];
|
||||||
@@ -184,6 +183,12 @@
|
|||||||
"flakes"
|
"flakes"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
users.users.nonrootuser = {
|
||||||
|
isNormalUser = true;
|
||||||
|
openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ];
|
||||||
|
extraGroups = [ "wheel" ];
|
||||||
|
};
|
||||||
|
security.sudo.wheelNeedsPassword = false;
|
||||||
system.extraDependencies = dependencies;
|
system.extraDependencies = dependencies;
|
||||||
};
|
};
|
||||||
nodes.client = {
|
nodes.client = {
|
||||||
@@ -211,14 +216,14 @@
|
|||||||
installer.start()
|
installer.start()
|
||||||
|
|
||||||
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
|
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
|
||||||
client.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v root@installer hostname")
|
client.wait_until_succeeds("timeout 2 ssh -o StrictHostKeyChecking=accept-new -v nonrootuser@installer hostname")
|
||||||
client.succeed("cp -r ${../..} test-flake && chmod -R +w test-flake")
|
client.succeed("cp -r ${../..} test-flake && chmod -R +w test-flake")
|
||||||
client.fail("test -f test-flake/machines/test-install-machine-without-system/hardware-configuration.nix")
|
client.fail("test -f test-flake/machines/test-install-machine-without-system/hardware-configuration.nix")
|
||||||
client.fail("test -f test-flake/machines/test-install-machine-without-system/facter.json")
|
client.fail("test -f test-flake/machines/test-install-machine-without-system/facter.json")
|
||||||
client.succeed("clan machines update-hardware-config --flake test-flake test-install-machine-without-system root@installer >&2")
|
client.succeed("clan machines update-hardware-config --flake test-flake test-install-machine-without-system nonrootuser@installer >&2")
|
||||||
client.succeed("test -f test-flake/machines/test-install-machine-without-system/facter.json")
|
client.succeed("test -f test-flake/machines/test-install-machine-without-system/facter.json")
|
||||||
client.succeed("rm test-flake/machines/test-install-machine-without-system/facter.json")
|
client.succeed("rm test-flake/machines/test-install-machine-without-system/facter.json")
|
||||||
client.succeed("clan machines install --debug --flake test-flake --yes test-install-machine-without-system --target-host root@installer --update-hardware-config nixos-facter >&2")
|
client.succeed("clan machines install --debug --flake test-flake --yes test-install-machine-without-system --target-host nonrootuser@installer --update-hardware-config nixos-facter >&2")
|
||||||
try:
|
try:
|
||||||
installer.shutdown()
|
installer.shutdown()
|
||||||
except BrokenPipeError:
|
except BrokenPipeError:
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
|
|||||||
]
|
]
|
||||||
|
|
||||||
host = machine.target_host
|
host = machine.target_host
|
||||||
|
|
||||||
|
# HACK: to make non-root user work
|
||||||
|
if host.user != "root":
|
||||||
|
config_command.insert(0, "sudo")
|
||||||
|
|
||||||
cmd = nix_shell(
|
cmd = nix_shell(
|
||||||
[
|
[
|
||||||
"nixpkgs#openssh",
|
"nixpkgs#openssh",
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ def deploy_machines(machines: list[Machine]) -> None:
|
|||||||
test_cmd,
|
test_cmd,
|
||||||
RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
||||||
extra_env=env,
|
extra_env=env,
|
||||||
|
become_root=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# retry nixos-rebuild switch if the first attempt failed
|
# retry nixos-rebuild switch if the first attempt failed
|
||||||
@@ -193,6 +194,7 @@ def deploy_machines(machines: list[Machine]) -> None:
|
|||||||
switch_cmd,
|
switch_cmd,
|
||||||
RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
|
||||||
extra_env=env,
|
extra_env=env,
|
||||||
|
become_root=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
with AsyncRuntime() as runtime:
|
with AsyncRuntime() as runtime:
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
import tarfile
|
import tarfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from shlex import quote
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from clan_cli.cmd import Log, RunOpts
|
from clan_cli.cmd import Log, RunOpts
|
||||||
from clan_cli.cmd import run as run_local
|
from clan_cli.cmd import run as run_local
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.ssh.host import Host
|
from clan_cli.ssh.host import Host
|
||||||
|
|
||||||
|
|
||||||
def upload(
|
def upload(
|
||||||
host: Host,
|
host: Host,
|
||||||
local_src: Path, # must be a directory
|
local_src: Path,
|
||||||
remote_dest: Path, # must be a directory
|
remote_dest: Path, # must be a directory
|
||||||
file_user: str = "root",
|
file_user: str = "root",
|
||||||
file_group: str = "root",
|
file_group: str = "root",
|
||||||
dir_mode: int = 0o700,
|
dir_mode: int = 0o700,
|
||||||
file_mode: int = 0o400,
|
file_mode: int = 0o400,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# Check if the remote destination is at least 3 directories deep
|
||||||
|
if len(remote_dest.parts) < 3:
|
||||||
|
msg = f"The remote destination must be at least 3 directories deep. Got: {remote_dest}. Reason: The directory will be deleted with 'rm -rf'."
|
||||||
|
raise ClanError(msg)
|
||||||
|
|
||||||
# Create the tarball from the temporary directory
|
# Create the tarball from the temporary directory
|
||||||
with TemporaryDirectory(prefix="facts-upload-") as tardir:
|
with TemporaryDirectory(prefix="facts-upload-") as tardir:
|
||||||
tar_path = Path(tardir) / "upload.tar.gz"
|
tar_path = Path(tardir) / "upload.tar.gz"
|
||||||
@@ -55,50 +62,22 @@ def upload(
|
|||||||
with local_src.open("rb") as f:
|
with local_src.open("rb") as f:
|
||||||
tar.addfile(tarinfo, f)
|
tar.addfile(tarinfo, f)
|
||||||
|
|
||||||
if local_src.is_dir():
|
sudo = ""
|
||||||
cmd = [
|
if host.user != "root":
|
||||||
*host.ssh_cmd(),
|
sudo = "sudo -- "
|
||||||
"rm",
|
|
||||||
"-r",
|
cmd = "rm -rf $0 && mkdir -m $1 -p $0 && tar -C $0 -xzf -"
|
||||||
str(remote_dest),
|
|
||||||
";",
|
|
||||||
"mkdir",
|
|
||||||
"-m",
|
|
||||||
f"{dir_mode:o}",
|
|
||||||
"-p",
|
|
||||||
str(remote_dest),
|
|
||||||
"&&",
|
|
||||||
"tar",
|
|
||||||
"-C",
|
|
||||||
str(remote_dest),
|
|
||||||
"-xzf",
|
|
||||||
"-",
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
# For single file, extract to parent directory and ensure correct name
|
|
||||||
cmd = [
|
|
||||||
*host.ssh_cmd(),
|
|
||||||
"rm",
|
|
||||||
"-f",
|
|
||||||
str(remote_dest),
|
|
||||||
";",
|
|
||||||
"mkdir",
|
|
||||||
"-m",
|
|
||||||
f"{dir_mode:o}",
|
|
||||||
"-p",
|
|
||||||
str(remote_dest.parent),
|
|
||||||
"&&",
|
|
||||||
"tar",
|
|
||||||
"-C",
|
|
||||||
str(remote_dest.parent),
|
|
||||||
"-xzf",
|
|
||||||
"-",
|
|
||||||
]
|
|
||||||
|
|
||||||
# TODO accept `input` to be an IO object instead of bytes so that we don't have to read the tarfile into memory.
|
# TODO accept `input` to be an IO object instead of bytes so that we don't have to read the tarfile into memory.
|
||||||
with tar_path.open("rb") as f:
|
with tar_path.open("rb") as f:
|
||||||
run_local(
|
run_local(
|
||||||
cmd,
|
[
|
||||||
|
*host.ssh_cmd(),
|
||||||
|
"--",
|
||||||
|
f"{sudo}bash -c {quote(cmd)}",
|
||||||
|
str(remote_dest),
|
||||||
|
f"{dir_mode:o}",
|
||||||
|
],
|
||||||
RunOpts(
|
RunOpts(
|
||||||
input=f.read(),
|
input=f.read(),
|
||||||
log=Log.BOTH,
|
log=Log.BOTH,
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ def sshd_config(test_root: Path) -> Iterator[SshdConfig]:
|
|||||||
)
|
)
|
||||||
config = tmpdir / "sshd_config"
|
config = tmpdir / "sshd_config"
|
||||||
config.write_text(content)
|
config.write_text(content)
|
||||||
login_shell = tmpdir / "shell"
|
bin_path = tmpdir / "bin"
|
||||||
|
login_shell = bin_path / "shell"
|
||||||
|
fake_sudo = bin_path / "sudo"
|
||||||
|
login_shell.parent.mkdir(parents=True)
|
||||||
|
|
||||||
bash = shutil.which("bash")
|
bash = shutil.which("bash")
|
||||||
path = os.environ["PATH"]
|
path = os.environ["PATH"]
|
||||||
@@ -65,19 +68,23 @@ def sshd_config(test_root: Path) -> Iterator[SshdConfig]:
|
|||||||
|
|
||||||
login_shell.write_text(
|
login_shell.write_text(
|
||||||
f"""#!{bash}
|
f"""#!{bash}
|
||||||
|
set -x
|
||||||
if [[ -f /etc/profile ]]; then
|
if [[ -f /etc/profile ]]; then
|
||||||
source /etc/profile
|
source /etc/profile
|
||||||
fi
|
fi
|
||||||
if [[ -n "$REALPATH" ]]; then
|
export PATH="{bin_path}:{path}"
|
||||||
export PATH="$REALPATH:${path}"
|
|
||||||
else
|
|
||||||
export PATH="${path}"
|
|
||||||
fi
|
|
||||||
exec {bash} -l "${{@}}"
|
exec {bash} -l "${{@}}"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
login_shell.chmod(0o755)
|
login_shell.chmod(0o755)
|
||||||
|
|
||||||
|
fake_sudo.write_text(
|
||||||
|
f"""#!{bash}
|
||||||
|
exec "${{@}}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fake_sudo.chmod(0o755)
|
||||||
|
|
||||||
lib_path = None
|
lib_path = None
|
||||||
|
|
||||||
extension = ".so"
|
extension = ".so"
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ def test_secrets_upload(
|
|||||||
monkeypatch.chdir(str(flake.path))
|
monkeypatch.chdir(str(flake.path))
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
|
|
||||||
|
sops_dir = flake.path / "facts"
|
||||||
|
|
||||||
|
# the flake defines this path as the location where the sops key should be installed
|
||||||
|
sops_key = sops_dir / "key.txt"
|
||||||
|
sops_key2 = sops_dir / "key2.txt"
|
||||||
|
|
||||||
|
# Create old state, which should be cleaned up
|
||||||
|
sops_dir.mkdir()
|
||||||
|
sops_key.write_text("OLD STATE")
|
||||||
|
sops_key2.write_text("OLD STATE2")
|
||||||
|
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"secrets",
|
"secrets",
|
||||||
@@ -56,8 +67,6 @@ def test_secrets_upload(
|
|||||||
|
|
||||||
cli.run(["facts", "upload", "--flake", str(flake_path), "vm1"])
|
cli.run(["facts", "upload", "--flake", str(flake_path), "vm1"])
|
||||||
|
|
||||||
# the flake defines this path as the location where the sops key should be installed
|
|
||||||
sops_key = flake.path / "facts" / "key.txt"
|
|
||||||
|
|
||||||
assert sops_key.exists()
|
assert sops_key.exists()
|
||||||
assert sops_key.read_text() == age_keys[0].privkey
|
assert sops_key.read_text() == age_keys[0].privkey
|
||||||
|
assert not sops_key2.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user