Clan-app: generate hw report

This commit is contained in:
Johannes Kirschbauer
2024-08-22 15:56:33 +02:00
parent 0b777dcf32
commit 2d119ae750
4 changed files with 183 additions and 21 deletions

View File

@@ -122,11 +122,15 @@ def generate_machine_hardware_info(
# Disable known hosts file # Disable known hosts file
"-o", "-o",
"UserKnownHostsFile=/dev/null", "UserKnownHostsFile=/dev/null",
"-p", # Disable strict host key checking. The GUI user cannot type "yes" into the ssh terminal.
str(machine.target_host.port), "-o",
"StrictHostKeyChecking=no",
*(
["-p", str(machine.target_host.port)]
if machine.target_host.port
else []
),
target_host, target_host,
"-o UserKnownHostsFile=/dev/null",
f"{hostname}",
"nixos-generate-config", "nixos-generate-config",
# Filesystems are managed by disko # Filesystems are managed by disko
"--no-filesystems", "--no-filesystems",

View File

@@ -1,5 +1,5 @@
/* @refresh reload */ /* @refresh reload */
import { render } from "solid-js/web"; import { Portal, render } from "solid-js/web";
import { RouteDefinition, Router } from "@solidjs/router"; import { RouteDefinition, Router } from "@solidjs/router";
import "./index.css"; import "./index.css";
@@ -14,6 +14,7 @@ import { Flash } from "./routes/flash/view";
import { CreateMachine } from "./routes/machines/create"; import { CreateMachine } from "./routes/machines/create";
import { HostList } from "./routes/hosts/view"; import { HostList } from "./routes/hosts/view";
import { Welcome } from "./routes/welcome"; import { Welcome } from "./routes/welcome";
import { Toaster } from "solid-toast";
const client = new QueryClient(); const client = new QueryClient();
@@ -113,9 +114,14 @@ export const routes: AppRoute[] = [
render( render(
() => ( () => (
<QueryClientProvider client={client}> <>
<Router root={Layout}>{routes}</Router> <Portal mount={document.body}>
</QueryClientProvider> <Toaster position="top-right" containerClassName="z-[9999]" />
</Portal>
<QueryClientProvider client={client}>
<Router root={Layout}>{routes}</Router>
</QueryClientProvider>
</>
), ),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
root!, root!,

View File

@@ -3,7 +3,6 @@ import { Header } from "./header";
import { Sidebar } from "../Sidebar"; import { Sidebar } from "../Sidebar";
import { activeURI, clanList } from "../App"; import { activeURI, clanList } from "../App";
import { redirect, RouteSectionProps, useNavigate } from "@solidjs/router"; import { redirect, RouteSectionProps, useNavigate } from "@solidjs/router";
import { Toaster } from "solid-toast";
export const Layout: Component<RouteSectionProps> = (props) => { export const Layout: Component<RouteSectionProps> = (props) => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -19,7 +18,6 @@ export const Layout: Component<RouteSectionProps> = (props) => {
}); });
return ( return (
<div class="h-screen bg-gradient-to-b from-white to-base-100 p-4"> <div class="h-screen bg-gradient-to-b from-white to-base-100 p-4">
<Toaster position="top-right" />
<div class="drawer lg:drawer-open "> <div class="drawer lg:drawer-open ">
<input <input
id="toplevel-drawer" id="toplevel-drawer"

View File

@@ -6,7 +6,7 @@ import { TextInput } from "@/src/components/TextInput";
import { createForm, getValue, reset } from "@modular-forms/solid"; import { createForm, getValue, reset } from "@modular-forms/solid";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { createQuery } from "@tanstack/solid-query"; import { createQuery } from "@tanstack/solid-query";
import { createSignal, For, Show, createEffect } from "solid-js"; import { createSignal, For, Show, Switch, Match } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
@@ -62,6 +62,27 @@ const InstallMachine = (props: InstallMachineProps) => {
const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk()); const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk());
const hwInfoQuery = createQuery(() => ({
queryKey: [
activeURI(),
"machine",
props.name,
"show_machine_hardware_info",
],
queryFn: async () => {
const curr = activeURI();
if (curr && props.name) {
const result = await callApi("show_machine_hardware_info", {
clan_dir: curr,
machine_name: props.name,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data || null;
}
return null;
},
}));
const handleInstall = async (values: InstallForm) => { const handleInstall = async (values: InstallForm) => {
console.log("Installing", values); console.log("Installing", values);
const curr_uri = activeURI(); const curr_uri = activeURI();
@@ -72,6 +93,9 @@ const InstallMachine = (props: InstallMachineProps) => {
return; return;
} }
const loading_toast = toast.loading(
"Installing machine. Grab coffee (15min)...",
);
const r = await callApi("install_machine", { const r = await callApi("install_machine", {
opts: { opts: {
flake: { flake: {
@@ -82,6 +106,7 @@ const InstallMachine = (props: InstallMachineProps) => {
}, },
password: "", password: "",
}); });
toast.dismiss(loading_toast);
if (r.status === "error") { if (r.status === "error") {
toast.error("Failed to install machine"); toast.error("Failed to install machine");
@@ -91,7 +116,8 @@ const InstallMachine = (props: InstallMachineProps) => {
} }
}; };
const handleDiskConfirm = async () => { const handleDiskConfirm = async (e: Event) => {
e.preventDefault();
const curr_uri = activeURI(); const curr_uri = activeURI();
const disk = getValue(formStore, "disk"); const disk = getValue(formStore, "disk");
const disk_id = props.disks.find((d) => d.name === disk)?.id_link; const disk_id = props.disks.find((d) => d.name === disk)?.id_link;
@@ -112,14 +138,86 @@ const InstallMachine = (props: InstallMachineProps) => {
setConfirmDisk(true); setConfirmDisk(true);
} }
}; };
const generateReport = async (e: Event) => {
e.preventDefault();
const curr_uri = activeURI();
if (!curr_uri || !props.name) {
return;
}
const loading_toast = toast.loading("Generating hardware report...");
const r = await callApi("generate_machine_hardware_info", {
clan_dir: { loc: curr_uri },
machine_name: props.name,
keyfile: props.sshKey?.name,
hostname: props.targetHost,
});
toast.dismiss(loading_toast);
hwInfoQuery.refetch();
if (r.status === "error") {
toast.error(`Failed to generate report. ${r.errors[0].message}`);
}
if (r.status === "success") {
toast.success("Report generated successfully");
}
};
return ( return (
<> <>
<Form onSubmit={handleInstall}> <Form onSubmit={handleInstall}>
<h3 class="text-lg font-bold">{props.name}</h3> <h3 class="text-lg font-bold">
<p class="py-4">Install to {props.targetHost}</p> <span class="font-normal">Install: </span>
{props.name}
</h3>
<p class="py-4"> <p class="py-4">
Using {props.sshKey?.name || "default ssh key"} for authentication Install the system for the first time. This will erase the disk and
bootstrap a new device.
</p> </p>
<div class="flex flex-col">
<div class="text-lg font-semibold">Hardware detection</div>
<div class="flex justify-between py-4">
<Switch>
<Match when={hwInfoQuery.isLoading}>
<span class="loading loading-lg"></span>
</Match>
<Match when={hwInfoQuery.isFetched}>
<Show
when={hwInfoQuery.data}
fallback={
<>
<span class="flex align-middle">
<span class="material-icons text-inherit">close</span>
Not Detected
</span>
<div class="text-neutral">
This might still work, but it is recommended to generate
a hardware report.
</div>
</>
}
>
<span class="flex align-middle">
<span class="material-icons text-inherit">check</span>
Detected
</span>
</Show>
</Match>
</Switch>
<div class="">
<button
class="btn btn-ghost btn-sm btn-wide"
onclick={generateReport}
>
<span class="material-icons">manage_search</span>
Generate report
</button>
</div>
</div>
</div>
<Field name="disk"> <Field name="disk">
{(field, fieldProps) => ( {(field, fieldProps) => (
<SelectInput <SelectInput
@@ -152,13 +250,26 @@ const InstallMachine = (props: InstallMachineProps) => {
/> />
)} )}
</Field> </Field>
<div role="alert" class="alert my-4">
<span class="material-icons">info</span>
<div>
<div class="font-semibold">Summary:</div>
<div class="mb-2">
Install to <b>{props.targetHost}</b> using{" "}
<b>{props.sshKey?.name || "default ssh key"}</b> for
authentication.
</div>
This may take ~15 minutes depending on the initial closure and the
environmental setup.
</div>
</div>
<div class="modal-action"> <div class="modal-action">
<Show <Show
when={confirmDisk()} when={confirmDisk()}
fallback={ fallback={
<button <button
class="btn btn-primary btn-wide" class="btn btn-primary btn-wide"
onClick={() => handleDiskConfirm()} onClick={handleDiskConfirm}
disabled={!hasDisk()} disabled={!hasDisk()}
> >
<span class="material-icons">check</span> <span class="material-icons">check</span>
@@ -211,7 +322,7 @@ const MachineForm = (props: MachineDetailsProps) => {
return result.data; return result.data;
} }
}, },
refetchInterval: 5000, // refetchInterval: 10_000, // 10 seconds
})); }));
const online = () => onlineStatusQuery.data === "Online"; const online = () => onlineStatusQuery.data === "Online";
@@ -268,6 +379,45 @@ const MachineForm = (props: MachineDetailsProps) => {
return null; return null;
}; };
const handleUpdate = async () => {
const curr_uri = activeURI();
if (!curr_uri) {
return;
}
const machine = machineName();
if (!machine) {
toast.error("Machine is required");
return;
}
const target = targetHost();
if (!target) {
toast.error("Target host is required");
return;
}
const loading_toast = toast.loading("Updating machine...");
const r = await callApi("update_machines", {
base_path: curr_uri,
machines: [
{
name: machine,
deploy: {
targetHost: target,
},
},
],
});
toast.dismiss(loading_toast);
if (r.status === "error") {
toast.error("Failed to update machine");
}
if (r.status === "success") {
toast.success("Machine updated successfully");
}
};
return ( return (
<div class="m-2 w-full max-w-xl"> <div class="m-2 w-full max-w-xl">
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
@@ -398,7 +548,7 @@ const MachineForm = (props: MachineDetailsProps) => {
</button> </button>
</div> </div>
<dialog id="install_modal" class="modal"> <dialog id="install_modal" class="modal backdrop:bg-transparent">
<div class="modal-box w-11/12 max-w-5xl"> <div class="modal-box w-11/12 max-w-5xl">
<InstallMachine <InstallMachine
name={machineName()} name={machineName()}
@@ -410,11 +560,15 @@ const MachineForm = (props: MachineDetailsProps) => {
</dialog> </dialog>
<span class="max-w-md text-neutral"> <span class="max-w-md text-neutral">
Installs the system for the first time. Used to bootstrap the remote Update the system if changes should be synced after the installation
device. process.
</span> </span>
<div class="tooltip w-fit" data-tip="Machine must be online"> <div class="tooltip w-fit" data-tip="Machine must be online">
<button class="btn btn-primary btn-sm btn-wide" disabled={!online()}> <button
class="btn btn-primary btn-sm btn-wide"
disabled={!online()}
onClick={() => handleUpdate()}
>
<span class="material-icons">update</span> <span class="material-icons">update</span>
Update Update
</button> </button>