UI: fix installer workflow asking for sudo pw in background

This commit is contained in:
Johannes Kirschbauer
2024-12-29 13:00:40 +01:00
parent 3404af9ccd
commit 87c9d1b941
4 changed files with 83 additions and 47 deletions

View File

@@ -7,13 +7,14 @@ from pathlib import Path
from clan_cli.cmd import Log, RunOpts, run from clan_cli.cmd import Log, RunOpts, run
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@contextmanager @contextmanager
def pause_automounting( def pause_automounting(
devices: list[Path], machine: Machine devices: list[Path], machine: Machine, sudo: str = "pkexec"
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
""" """
Pause automounting on the device for the duration of this context Pause automounting on the device for the duration of this context
@@ -32,7 +33,11 @@ def pause_automounting(
raise ClanError(msg) raise ClanError(msg)
str_devs = [str(dev) for dev in devices] str_devs = [str(dev) for dev in devices]
cmd = ["sudo", str(inhibit_path), "enable", *str_devs] cmd = nix_shell(
["nixpkgs#pkexec"] if sudo == "pkexec" else [],
[sudo, str(inhibit_path), "enable", *str_devs],
)
result = run( result = run(
cmd, cmd,
RunOpts( RunOpts(
@@ -42,7 +47,7 @@ def pause_automounting(
if result.returncode != 0: if result.returncode != 0:
machine.error("Failed to inhibit automounting") machine.error("Failed to inhibit automounting")
yield None yield None
cmd = ["sudo", str(inhibit_path), "disable", *str_devs] cmd = [sudo, str(inhibit_path), "disable", *str_devs]
result = run(cmd, RunOpts(log=Log.BOTH, check=False, prefix=machine.name)) result = run(cmd, RunOpts(log=Log.BOTH, check=False, prefix=machine.name))
if result.returncode != 0: if result.returncode != 0:
machine.error("Failed to re-enable automounting") machine.error("Failed to re-enable automounting")

View File

@@ -47,9 +47,11 @@ def flash_machine(
write_efi_boot_entries: bool, write_efi_boot_entries: bool,
debug: bool, debug: bool,
extra_args: list[str] | None = None, extra_args: list[str] | None = None,
use_user_permission: bool = False,
) -> None: ) -> None:
devices = [Path(disk.device) for disk in disks] devices = [Path(disk.device) for disk in disks]
with pause_automounting(devices, machine): sudo = "pkexec" if use_user_permission else "sudo"
with pause_automounting(devices, machine, sudo):
if extra_args is None: if extra_args is None:
extra_args = [] extra_args = []
system_config_nix: dict[str, Any] = {} system_config_nix: dict[str, Any] = {}
@@ -108,10 +110,12 @@ def flash_machine(
disko_install = [] disko_install = []
if os.geteuid() != 0: if os.geteuid() != 0:
# Use pkexec to elevate permissions if not running as root
perm_prefix = "pkexec" if use_user_permission else "exec sudo"
if shutil.which("sudo") is None: if shutil.which("sudo") is None:
msg = "sudo is required to run disko-install as a non-root user" msg = "sudo is required to run disko-install as a non-root user"
raise ClanError(msg) raise ClanError(msg)
wrapper = 'set -x; disko_install=$(command -v disko-install); exec sudo "$disko_install" "$@"' wrapper = f'set -x; disko_install=$(command -v disko-install); {perm_prefix} "$disko_install" "$@"'
disko_install.extend(["bash", "-c", wrapper]) disko_install.extend(["bash", "-c", wrapper])
disko_install.append("disko-install") disko_install.append("disko-install")
@@ -124,6 +128,8 @@ def flash_machine(
for disk in disks: for disk in disks:
disko_install.extend(["--disk", disk.name, disk.device]) 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(["--extra-files", str(local_dir), upload_dir])
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name]) disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
disko_install.extend(["--mode", str(mode)]) disko_install.extend(["--mode", str(mode)])

View File

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

View File

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