Merge pull request 'UI: fix installer workflow asking for sudo pw in background' (#2663) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-12-30 14:52:41 +00:00
5 changed files with 135 additions and 54 deletions

View File

@@ -4,6 +4,7 @@ import math
import os
import select
import shlex
import shutil
import signal
import subprocess
import threading
@@ -271,6 +272,38 @@ class RunOpts:
needs_user_terminal: bool = False
timeout: float = math.inf
shell: bool = False
# Some commands require sudo
requires_root_perm: bool = False
# Ask for sudo password in a graphical way.
# This is needed for GUI applications
graphical_perm: bool = False
def cmd_with_root(cmd: list[str], graphical: bool = False) -> list[str]:
"""
This function returns a wrapped command that will be run with root permissions.
It will use sudo if graphical is False, otherwise it will use run0 or pkexec.
"""
if os.geteuid() == 0:
return cmd
# Decide permission handler
if graphical:
# TODO(mic92): figure out how to use run0
# if shutil.which("run0") is not None:
# perm_prefix = "run0"
if shutil.which("pkexec") is not None:
return ["pkexec", *cmd]
description = (
"pkexec is required to launch root commands with graphical permissions"
)
msg = "Missing graphical permission handler"
raise ClanError(msg, description=description)
if shutil.which("sudo") is None:
msg = "sudo is required to run this command as a non-root user"
raise ClanError(msg)
return ["sudo", *cmd]
def run(
@@ -293,6 +326,9 @@ def run(
if options.stderr is None:
options.stderr = async_ctx.stderr
if options.requires_root_perm:
cmd = cmd_with_root(cmd, options.graphical_perm)
if options.input:
if any(not ch.isprintable() for ch in options.input.decode("ascii", "replace")):
filtered_input = "<<binary_blob>>"

View File

@@ -13,7 +13,7 @@ log = logging.getLogger(__name__)
@contextmanager
def pause_automounting(
devices: list[Path], machine: Machine
devices: list[Path], machine: Machine, request_graphical: bool = False
) -> Generator[None, None, None]:
"""
Pause automounting on the device for the duration of this context
@@ -32,17 +32,32 @@ def pause_automounting(
raise ClanError(msg)
str_devs = [str(dev) for dev in devices]
cmd = ["sudo", str(inhibit_path), "enable", *str_devs]
cmd = [str(inhibit_path), "enable", *str_devs]
result = run(
cmd,
RunOpts(
log=Log.BOTH, check=False, needs_user_terminal=True, prefix=machine.name
log=Log.BOTH,
check=False,
needs_user_terminal=True,
prefix=machine.name,
requires_root_perm=True,
graphical_perm=request_graphical,
),
)
if result.returncode != 0:
machine.error("Failed to inhibit automounting")
yield None
cmd = ["sudo", str(inhibit_path), "disable", *str_devs]
result = run(cmd, RunOpts(log=Log.BOTH, check=False, prefix=machine.name))
cmd = [str(inhibit_path), "disable", *str_devs]
result = run(
cmd,
RunOpts(
log=Log.BOTH,
check=False,
prefix=machine.name,
requires_root_perm=True,
graphical_perm=request_graphical,
),
)
if result.returncode != 0:
machine.error("Failed to re-enable automounting")

View File

@@ -2,14 +2,13 @@ import importlib
import json
import logging
import os
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from clan_cli.api import API
from clan_cli.cmd import Log, RunOpts, run
from clan_cli.cmd import Log, RunOpts, cmd_with_root, run
from clan_cli.errors import ClanError
from clan_cli.facts.generate import generate_facts
from clan_cli.facts.secret_modules import SecretStoreBase
@@ -47,9 +46,10 @@ def flash_machine(
write_efi_boot_entries: bool,
debug: bool,
extra_args: list[str] | None = None,
graphical: bool = False,
) -> None:
devices = [Path(disk.device) for disk in disks]
with pause_automounting(devices, machine):
with pause_automounting(devices, machine, request_graphical=graphical):
if extra_args is None:
extra_args = []
system_config_nix: dict[str, Any] = {}
@@ -108,10 +108,13 @@ def flash_machine(
disko_install = []
if os.geteuid() != 0:
if shutil.which("sudo") is None:
msg = "sudo is required to run disko-install as a non-root user"
raise ClanError(msg)
wrapper = 'set -x; disko_install=$(command -v disko-install); exec sudo "$disko_install" "$@"'
wrapper = " ".join(
[
"disko_install=$(command -v disko-install);",
"exec",
*cmd_with_root(['"$disko_install" "$@"'], graphical=graphical),
]
)
disko_install.extend(["bash", "-c", wrapper])
disko_install.append("disko-install")
@@ -124,6 +127,8 @@ def flash_machine(
for disk in disks:
disko_install.extend(["--disk", disk.name, disk.device])
log.info("Will flash disk %s: %s", disk.name, disk.device)
disko_install.extend(["--extra-files", str(local_dir), upload_dir])
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
disko_install.extend(["--mode", str(mode)])

View File

@@ -1,5 +1,5 @@
import Dialog from "corvu/dialog";
import { createEffect, createSignal, JSX } from "solid-js";
import { createSignal, JSX } from "solid-js";
import { Button } from "../button";
import Icon from "../icon";
import cx from "classnames";
@@ -13,7 +13,7 @@ interface ModalProps {
export const Modal = (props: ModalProps) => {
const [dragging, setDragging] = createSignal(false);
const [startOffset, setStartOffset] = createSignal({ x: 0, y: 0 });
// const [dialogStyle, setDialogStyle] = createSignal({ top: 100, left: 100 });
let dialogRef: HTMLDivElement;
const handleMouseDown = (e: MouseEvent) => {
@@ -37,9 +37,6 @@ export const Modal = (props: ModalProps) => {
const handleMouseUp = () => setDragging(false);
createEffect(() => {
console.log("dialog open", props.open);
});
return (
<Dialog open={props.open} trapFocus={true}>
<Dialog.Portal>

View File

@@ -14,6 +14,7 @@ import {
FieldValues,
setValue,
getValue,
getValues,
} from "@modular-forms/solid";
import { createQuery } from "@tanstack/solid-query";
import { createEffect, createSignal, For, Show } from "solid-js";
@@ -146,63 +147,89 @@ export const Flash = () => {
return dataTransfer.files;
};
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [isFlashing, setFlashing] = createSignal(false);
const handleConfirm = async (values: FlashFormValues) => {
const handleSubmit = (values: FlashFormValues) => {
setConfirmOpen(true);
console.log("Submit:", values);
toast.error("Not fully implemented yet");
// Disabled for now. To prevent accidental flashing of local disks
// User should confirm the disk to flash to
// try {
// await callApi("flash_machine", {
// machine: {
// name: values.machine.devicePath,
// flake: {
// loc: values.machine.flake,
// },
// },
// mode: "format",
// disks: [{ name: "main", device: values.disk }],
// system_config: {
// language: values.language,
// keymap: values.keymap,
// ssh_keys_path: values.sshKeys.map((file) => file.name),
// },
// dry_run: false,
// write_efi_boot_entries: false,
// debug: false,
// });
// } catch (error) {
// toast.error(`Error could not flash disk: ${error}`);
// console.error("Error submitting form:", error);
// }
};
const handleConfirm = async () => {
// Wait for the flash to complete
const values = getValues(formStore) as FlashFormValues;
setFlashing(true);
console.log("Confirmed flash:", values);
try {
await toast.promise(
callApi("flash_machine", {
machine: {
name: values.machine.devicePath,
flake: {
loc: values.machine.flake,
},
},
mode: "format",
disks: [{ name: "main", device: values.disk }],
system_config: {
language: values.language,
keymap: values.keymap,
ssh_keys_path: values.sshKeys.map((file) => file.name),
},
dry_run: false,
write_efi_boot_entries: false,
debug: false,
graphical: true,
}),
{
error: (errors) => `Error flashing disk: ${errors}`,
loading: "Flashing ... This may take up to 15minutes.",
success: "Disk flashed successfully",
},
);
} catch (error) {
toast.error(`Error could not flash disk: ${error}`);
} finally {
setFlashing(false);
}
setConfirmOpen(false);
};
return (
<>
<Header title="Flash installer" />
<Modal
open={confirmOpen()}
handleClose={() => setConfirmOpen(false)}
open={confirmOpen() || isFlashing()}
handleClose={() => !isFlashing() && setConfirmOpen(false)}
title="Confirm"
>
<div class="flex flex-col gap-4 p-4">
<div class="flex justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2">
<div class="flex flex-col justify-between rounded-sm border p-4 align-middle text-red-900 border-def-2">
<Typography
hierarchy="label"
weight="medium"
size="default"
class="flex-wrap break-words pr-4"
>
Warning: All data on will be lost.
<br />
Warning: All data will be lost.
</Typography>
<Typography
hierarchy="label"
weight="bold"
size="default"
class="flex-wrap break-words pr-4"
>
Selected disk: '{getValue(formStore, "disk")}'
</Typography>
</div>
<div class="flex w-full justify-between">
<Button variant="light">Cancel</Button>
<Button>Confirm</Button>
<Button
disabled={isFlashing()}
variant="light"
onClick={() => setConfirmOpen(false)}
>
Cancel
</Button>
<Button disabled={isFlashing()} onClick={handleConfirm}>
Confirm
</Button>
</div>
</div>
</Modal>
@@ -214,7 +241,7 @@ export const Flash = () => {
Will make bootstrapping new machines easier by providing secure remote
connection to any machine when plugged in.
</Typography>
<Form onSubmit={handleConfirm}>
<Form onSubmit={handleSubmit}>
<div class="my-4">
<Field name="sshKeys" type="File[]">
{(field, props) => (
@@ -259,6 +286,7 @@ export const Flash = () => {
labelProps={{
labelAction: (
<Button
disabled={isFlashing()}
class="ml-auto"
variant="ghost"
size="s"