UI: fix installer workflow asking for sudo pw in background
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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)])
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user