Merge pull request 'Disk workflow improvements.' (#2692) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -3,7 +3,7 @@ import logging
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, TypedDict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
@@ -124,6 +124,11 @@ def get_disk_schemas(
|
|||||||
return disk_schemas
|
return disk_schemas
|
||||||
|
|
||||||
|
|
||||||
|
class MachineDiskMatter(TypedDict):
|
||||||
|
schema_name: str
|
||||||
|
placeholders: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def set_machine_disk_schema(
|
def set_machine_disk_schema(
|
||||||
base_path: Path,
|
base_path: Path,
|
||||||
@@ -134,7 +139,7 @@ def set_machine_disk_schema(
|
|||||||
placeholders: dict[str, str],
|
placeholders: dict[str, str],
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
""" "
|
"""
|
||||||
Set the disk placeholders of the template
|
Set the disk placeholders of the template
|
||||||
"""
|
"""
|
||||||
# Assert the hw-config must exist before setting the disk
|
# Assert the hw-config must exist before setting the disk
|
||||||
@@ -183,8 +188,13 @@ def set_machine_disk_schema(
|
|||||||
)
|
)
|
||||||
raise ClanError(msg, description=f"Valid options: {ph.options}")
|
raise ClanError(msg, description=f"Valid options: {ph.options}")
|
||||||
|
|
||||||
|
placeholders_toml = "\n".join(
|
||||||
|
[f"""# {k} = "{v}""" for k, v in placeholders.items() if v is not None]
|
||||||
|
)
|
||||||
header = f"""# ---
|
header = f"""# ---
|
||||||
# schema = "{schema_name}"
|
# schema = "{schema_name}"
|
||||||
|
# [placeholders]
|
||||||
|
{placeholders_toml}
|
||||||
# ---
|
# ---
|
||||||
# This file was automatically generated!
|
# This file was automatically generated!
|
||||||
# CHANGING this configuration requires wiping and reinstalling the machine
|
# CHANGING this configuration requires wiping and reinstalling the machine
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
|
from clan_cli.api.disk import MachineDiskMatter
|
||||||
from clan_cli.api.modules import parse_frontmatter
|
from clan_cli.api.modules import parse_frontmatter
|
||||||
from clan_cli.api.serde import dataclass_to_dict
|
from clan_cli.api.serde import dataclass_to_dict
|
||||||
from clan_cli.cmd import RunOpts, run_no_stdout
|
from clan_cli.cmd import RunOpts, run_no_stdout
|
||||||
@@ -41,7 +42,7 @@ def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
|||||||
class MachineDetails:
|
class MachineDetails:
|
||||||
machine: Machine
|
machine: Machine
|
||||||
hw_config: HardwareConfig | None = None
|
hw_config: HardwareConfig | None = None
|
||||||
disk_schema: str | None = None
|
disk_schema: MachineDiskMatter | None = None
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@@ -69,7 +70,7 @@ def get_inventory_machine_details(flake_url: Path, machine_name: str) -> Machine
|
|||||||
hw_config = HardwareConfig.detect_type(flake_url, machine_name)
|
hw_config = HardwareConfig.detect_type(flake_url, machine_name)
|
||||||
|
|
||||||
machine_dir = specific_machine_dir(flake_url, machine_name)
|
machine_dir = specific_machine_dir(flake_url, machine_name)
|
||||||
disk_schema: str | None = None
|
disk_schema: MachineDiskMatter | None = None
|
||||||
disk_path = machine_dir / "disko.nix"
|
disk_path = machine_dir / "disko.nix"
|
||||||
if disk_path.exists():
|
if disk_path.exists():
|
||||||
with disk_path.open() as f:
|
with disk_path.open() as f:
|
||||||
@@ -77,7 +78,7 @@ def get_inventory_machine_details(flake_url: Path, machine_name: str) -> Machine
|
|||||||
header = extract_header(content)
|
header = extract_header(content)
|
||||||
data, _rest = parse_frontmatter(header)
|
data, _rest = parse_frontmatter(header)
|
||||||
if data:
|
if data:
|
||||||
disk_schema = data.get("schema")
|
disk_schema = data # type: ignore
|
||||||
|
|
||||||
return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema)
|
return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema)
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ export function SelectInput(props: SelectInputpProps) {
|
|||||||
// Currently the popover only opens with onClick
|
// Currently the popover only opens with onClick
|
||||||
// Options are not selectable with keyboard
|
// Options are not selectable with keyboard
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
disabled={props.disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const popover = document.getElementById(_id);
|
const popover = document.getElementById(_id);
|
||||||
if (popover) {
|
if (popover) {
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import { InputLabel } from "@/src/components/inputBase";
|
|||||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||||
import { Modal } from "@/src/components/modal";
|
import { Modal } from "@/src/components/modal";
|
||||||
import { Typography } from "@/src/components/Typography";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import cx from "classnames";
|
|
||||||
import { HardwareValues, HWStep } from "./install/hardware-step";
|
import { HardwareValues, HWStep } from "./install/hardware-step";
|
||||||
import { DiskStep, DiskValues } from "./install/disk-step";
|
import { DiskStep, DiskValues } from "./install/disk-step";
|
||||||
import { SummaryStep } from "./install/summary-step";
|
import { SummaryStep } from "./install/summary-step";
|
||||||
|
import cx from "classnames";
|
||||||
import { SectionHeader } from "@/src/components/group";
|
import { SectionHeader } from "@/src/components/group";
|
||||||
|
|
||||||
type MachineFormInterface = MachineData & {
|
type MachineFormInterface = MachineData & {
|
||||||
@@ -48,12 +48,30 @@ export interface AllStepsValues extends FieldValues {
|
|||||||
"3": NonNullable<unknown>;
|
"3": NonNullable<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LoadingBar = () => (
|
||||||
|
<div
|
||||||
|
class="h-3 w-80 overflow-hidden rounded-[3px] border-2 border-def-1"
|
||||||
|
style={{
|
||||||
|
background: `repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#ccc,
|
||||||
|
#ccc 8px,
|
||||||
|
#eee 8px,
|
||||||
|
#eee 16px
|
||||||
|
)`,
|
||||||
|
animation: "slide 25s linear infinite",
|
||||||
|
"background-size": "200% 100%",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
|
||||||
function sleep(ms: number) {
|
function sleep(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
interface InstallMachineProps {
|
interface InstallMachineProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
targetHost?: string | null;
|
targetHost?: string | null;
|
||||||
|
machine: MachineData;
|
||||||
}
|
}
|
||||||
const InstallMachine = (props: InstallMachineProps) => {
|
const InstallMachine = (props: InstallMachineProps) => {
|
||||||
const curr = activeURI();
|
const curr = activeURI();
|
||||||
@@ -64,9 +82,9 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
|
|
||||||
const [formStore, { Form, Field }] = createForm<AllStepsValues>();
|
const [formStore, { Form, Field }] = createForm<AllStepsValues>();
|
||||||
|
|
||||||
|
const [isDone, setIsDone] = createSignal<boolean>(false);
|
||||||
const [isInstalling, setIsInstalling] = createSignal<boolean>(false);
|
const [isInstalling, setIsInstalling] = createSignal<boolean>(false);
|
||||||
const [progressText, setProgressText] = createSignal<string>();
|
const [progressText, setProgressText] = createSignal<string>();
|
||||||
const [installError, setInstallError] = createSignal<string>();
|
|
||||||
|
|
||||||
const handleInstall = async (values: AllStepsValues) => {
|
const handleInstall = async (values: AllStepsValues) => {
|
||||||
console.log("Installing", values);
|
console.log("Installing", values);
|
||||||
@@ -86,42 +104,36 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
"Installing machine. Grab coffee (15min)...",
|
"Installing machine. Grab coffee (15min)...",
|
||||||
);
|
);
|
||||||
setIsInstalling(true);
|
setIsInstalling(true);
|
||||||
setProgressText("Setting up disk ... (1/5)");
|
|
||||||
|
|
||||||
const disk_response = await callApi("set_machine_disk_schema", {
|
// props.machine.disk_
|
||||||
base_path: curr_uri,
|
const shouldRunDisk =
|
||||||
machine_name: props.name,
|
JSON.stringify(props.machine.disk_schema?.placeholders) !==
|
||||||
placeholders: diskValues.placeholders,
|
JSON.stringify(diskValues.placeholders);
|
||||||
schema_name: diskValues.schema,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (disk_response.status === "error") {
|
if (shouldRunDisk) {
|
||||||
toast.error(
|
setProgressText("Setting up disk ... (1/5)");
|
||||||
`Failed to set disk schema: ${disk_response.errors[0].message}`,
|
const disk_response = await callApi("set_machine_disk_schema", {
|
||||||
);
|
base_path: curr_uri,
|
||||||
setProgressText(
|
machine_name: props.name,
|
||||||
"Failed to set disk schema. \n" + disk_response.errors[0].message,
|
placeholders: diskValues.placeholders,
|
||||||
);
|
schema_name: diskValues.schema,
|
||||||
return;
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (disk_response.status === "error") {
|
||||||
|
toast.error(
|
||||||
|
`Failed to set disk schema: ${disk_response.errors[0].message}`,
|
||||||
|
);
|
||||||
|
setProgressText(
|
||||||
|
"Failed to set disk schema. \n" + disk_response.errors[0].message,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next step
|
setProgressText("Installing machine ... (2/5)");
|
||||||
if (disk_response.status === "success") {
|
|
||||||
setProgressText("Evaluate configuration ... (2/5)");
|
|
||||||
}
|
|
||||||
// Next step
|
|
||||||
await sleep(2000);
|
|
||||||
setProgressText("Building machine ... (3/5)");
|
|
||||||
await sleep(2000);
|
|
||||||
setProgressText("Formatting remote disk ... (4/5)");
|
|
||||||
await sleep(2000);
|
|
||||||
setProgressText("Copying system ... (5/5)");
|
|
||||||
await sleep(2000);
|
|
||||||
setProgressText("Rebooting remote system ... ");
|
|
||||||
await sleep(2000);
|
|
||||||
|
|
||||||
const r = await callApi("install_machine", {
|
const installPromise = callApi("install_machine", {
|
||||||
opts: {
|
opts: {
|
||||||
machine: {
|
machine: {
|
||||||
name: props.name,
|
name: props.name,
|
||||||
@@ -133,13 +145,36 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Next step
|
||||||
|
await sleep(10 * 1000);
|
||||||
|
setProgressText("Building machine ... (3/5)");
|
||||||
|
await sleep(10 * 1000);
|
||||||
|
setProgressText("Formatting remote disk ... (4/5)");
|
||||||
|
await sleep(10 * 1000);
|
||||||
|
setProgressText("Copying system ... (5/5)");
|
||||||
|
await sleep(20 * 1000);
|
||||||
|
setProgressText("Rebooting remote system ... ");
|
||||||
|
await sleep(10 * 1000);
|
||||||
|
|
||||||
|
const installResponse = await installPromise;
|
||||||
|
|
||||||
toast.dismiss(loading_toast);
|
toast.dismiss(loading_toast);
|
||||||
|
|
||||||
if (r.status === "error") {
|
if (installResponse.status === "error") {
|
||||||
toast.error("Failed to install machine");
|
toast.error("Failed to install machine");
|
||||||
|
setIsDone(true);
|
||||||
|
setProgressText(
|
||||||
|
"Failed to install machine. \n" + installResponse.errors[0].message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (r.status === "success") {
|
|
||||||
|
if (installResponse.status === "success") {
|
||||||
toast.success("Machine installed successfully");
|
toast.success("Machine installed successfully");
|
||||||
|
setIsDone(true);
|
||||||
|
setProgressText(
|
||||||
|
"Machine installed successfully. Please unplug the usb stick and reboot the system.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,7 +306,12 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
setValue(formStore, "2", { ...prev, ...data });
|
setValue(formStore, "2", { ...prev, ...data });
|
||||||
handleNext();
|
handleNext();
|
||||||
}}
|
}}
|
||||||
initial={getValue(formStore, "2")}
|
// @ts-expect-error: The placeholder type is to wide
|
||||||
|
initial={{
|
||||||
|
...props.machine.disk_schema,
|
||||||
|
...getValue(formStore, "2"),
|
||||||
|
initialized: !!props.machine.disk_schema,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={step() === "3"}>
|
<Match when={step() === "3"}>
|
||||||
@@ -326,20 +366,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex h-full flex-col items-center gap-3 px-4 py-8 bg-inv-4 fg-inv-1">
|
<div class="flex h-full flex-col items-center gap-3 px-4 py-8 bg-inv-4 fg-inv-1">
|
||||||
<Icon icon="ClanIcon" viewBox="0 0 72 89" class="size-20" />
|
<Icon icon="ClanIcon" viewBox="0 0 72 89" class="size-20" />
|
||||||
<div
|
{isDone() && <LoadingBar />}
|
||||||
class="h-3 w-80 overflow-hidden rounded-[3px] border-2 border-def-1"
|
|
||||||
style={{
|
|
||||||
background: `repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
#ccc,
|
|
||||||
#ccc 8px,
|
|
||||||
#eee 8px,
|
|
||||||
#eee 16px
|
|
||||||
)`,
|
|
||||||
animation: "slide 25s linear infinite",
|
|
||||||
"background-size": "200% 100%",
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="default"
|
size="default"
|
||||||
@@ -500,7 +527,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="disk_schema">
|
<Field name="disk_schema.schema_name">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<FieldLayout
|
<FieldLayout
|
||||||
@@ -576,6 +603,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
<InstallMachine
|
<InstallMachine
|
||||||
name={machineName()}
|
name={machineName()}
|
||||||
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
||||||
|
machine={props.initialData}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface DiskValues extends FieldValues {
|
|||||||
mainDisk: string;
|
mainDisk: string;
|
||||||
};
|
};
|
||||||
schema: string;
|
schema: string;
|
||||||
|
initialized: boolean;
|
||||||
}
|
}
|
||||||
export const DiskStep = (props: StepProps<DiskValues>) => {
|
export const DiskStep = (props: StepProps<DiskValues>) => {
|
||||||
const [formStore, { Form, Field }] = createForm<DiskValues>({
|
const [formStore, { Form, Field }] = createForm<DiskValues>({
|
||||||
@@ -74,9 +75,14 @@ export const DiskStep = (props: StepProps<DiskValues>) => {
|
|||||||
</Field>
|
</Field>
|
||||||
</span>
|
</span>
|
||||||
<Group>
|
<Group>
|
||||||
|
{props.initial?.initialized && "Disk has been initialized already"}
|
||||||
<Field
|
<Field
|
||||||
name="placeholders.mainDisk"
|
name="placeholders.mainDisk"
|
||||||
validate={required("Dsik must be provided")}
|
validate={
|
||||||
|
!props.initial?.initialized
|
||||||
|
? required("Disk must be provided")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{(field, fieldProps) => (
|
{(field, fieldProps) => (
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -88,18 +94,12 @@ export const DiskStep = (props: StepProps<DiskValues>) => {
|
|||||||
{ label: "No options", value: "" },
|
{ label: "No options", value: "" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
// options={
|
|
||||||
// deviceQuery.data?.blockdevices.map((d) => ({
|
|
||||||
// value: d.path,
|
|
||||||
// label: `${d.path} -- ${d.size} bytes`,
|
|
||||||
// })) || []
|
|
||||||
// }
|
|
||||||
error={field.error}
|
error={field.error}
|
||||||
label="Main Disk"
|
label="Main Disk"
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
placeholder="Select a disk"
|
placeholder="Select a disk"
|
||||||
selectProps={fieldProps}
|
selectProps={fieldProps}
|
||||||
required
|
required={!props.initial?.initialized}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
Reference in New Issue
Block a user