Clan-app: generate hw report
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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!,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user