Clan-app: generate hw spec via ssh

This commit is contained in:
Johannes Kirschbauer
2024-08-14 16:16:51 +02:00
parent eb844e83fe
commit 3f46d37b67
8 changed files with 148 additions and 29 deletions

View File

@@ -114,7 +114,7 @@ def generate_machine_hardware_info(
"-o StrictHostKeyChecking=no", "-o StrictHostKeyChecking=no",
# Disable known hosts file # Disable known hosts file
"-o UserKnownHostsFile=/dev/null", "-o UserKnownHostsFile=/dev/null",
f"root@{hostname}", 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,6 +1,7 @@
import argparse import argparse
import json import json
import logging import logging
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from clan_cli.api import API from clan_cli.api import API
@@ -18,6 +19,33 @@ def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
return inventory.machines return inventory.machines
@dataclass
class MachineDetails:
machine: Machine
has_hw_specs: bool = False
# TODO:
# has_disk_specs: bool = False
@API.register
def get_inventory_machine_details(
flake_url: str | Path, machine_name: str
) -> MachineDetails:
inventory = load_inventory_eval(flake_url)
machine = inventory.machines.get(machine_name)
if machine is None:
raise ClanError(f"Machine {machine_name} not found in inventory")
hw_config_path = (
Path(flake_url) / "machines" / Path(machine_name) / "hardware-configuration.nix"
)
return MachineDetails(
machine=machine,
has_hw_specs=hw_config_path.exists(),
)
@API.register @API.register
def list_nixos_machines(flake_url: str | Path) -> list[str]: def list_nixos_machines(flake_url: str | Path) -> list[str]:
cmd = nix_eval( cmd = nix_eval(

View File

@@ -21,10 +21,6 @@ def test_machine_subcommands(
assert "vm2" in out.out assert "vm2" in out.out
capsys.readouterr() capsys.readouterr()
cli.run(["machines", "show", "--flake", str(test_flake_with_core.path), "machine1"])
out = capsys.readouterr()
assert "machine1" in out.out
assert "Description" in out.out
print(out) print(out)
cli.run( cli.run(

View File

@@ -3,6 +3,7 @@ import { callApi, SuccessData } from "../api";
import { Menu } from "./Menu"; import { Menu } from "./Menu";
import { activeURI } from "../App"; import { activeURI } from "../App";
import toast from "solid-toast"; import toast from "solid-toast";
import { useNavigate } from "@solidjs/router";
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string]; type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
@@ -16,7 +17,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
const { name, info, nixOnly } = props; const { name, info, nixOnly } = props;
const [deploying, setDeploying] = createSignal<boolean>(false); const [deploying, setDeploying] = createSignal<boolean>(false);
const navigate = useNavigate();
return ( return (
<li> <li>
<div class="card card-side m-2 bg-base-200"> <div class="card card-side m-2 bg-base-200">
@@ -63,11 +64,11 @@ export const MachineListItem = (props: MachineListItemProps) => {
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow"> <ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
<li> <li>
<a <a
// onClick={() => { onClick={() => {
// setRoute("machines/edit"); navigate("/machines/" + name);
// }} }}
> >
Edit Details
</a> </a>
</li> </li>
<li <li

View File

@@ -3,10 +3,15 @@ import { Header } from "./header";
import { Sidebar } from "../Sidebar"; import { Sidebar } from "../Sidebar";
import { activeURI, clanList } from "../App"; import { activeURI, clanList } from "../App";
import { RouteSectionProps } from "@solidjs/router"; import { RouteSectionProps } from "@solidjs/router";
import { Toaster } from "solid-toast";
export const Layout: Component<RouteSectionProps> = (props) => { export const Layout: Component<RouteSectionProps> = (props) => {
createEffect(() => {
console.log("Layout props", props.location);
});
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

@@ -1,6 +1,110 @@
import { callApi } from "@/src/api";
import { activeURI } from "@/src/App";
import { useParams } from "@solidjs/router"; import { useParams } from "@solidjs/router";
import { createQuery } from "@tanstack/solid-query";
import { createSignal, Show } from "solid-js";
import toast from "solid-toast";
export const MachineDetails = () => { export const MachineDetails = () => {
const params = useParams(); const params = useParams();
return <div>Machine Details: {params.id}</div>; const query = createQuery(() => ({
queryKey: [activeURI(), "machine", params.id],
queryFn: async () => {
const curr = activeURI();
if (curr) {
const result = await callApi("get_inventory_machine_details", {
flake_url: curr,
machine_name: params.id,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
}
},
}));
const [sshKey, setSshKey] = createSignal<string>();
return (
<div>
{query.isLoading && <span class="loading loading-bars" />}
<Show when={!query.isLoading && query.data}>
{(data) => (
<div class="grid grid-cols-2 gap-2 text-lg">
<label class="justify-self-end font-light">Name</label>
<span>{data().machine.name}</span>
<span class="justify-self-end font-light">description</span>
<span>{data().machine.description}</span>
<span class="justify-self-end font-light">targetHost</span>
<span>{data().machine.deploy.targetHost}</span>
<div class="join col-span-2 justify-self-center">
<button
class="btn join-item btn-sm"
onClick={async () => {
const response = await callApi("open_file", {
file_request: {
title: "Select SSH Key",
mode: "open_file",
initial_folder: "~/.ssh",
},
});
if (response.status === "success" && response.data) {
setSshKey(response.data[0]);
}
}}
>
<span>{sshKey() || "Default ssh key"}</span>
</button>
<button
disabled={!sshKey()}
class="btn btn-accent join-item btn-sm"
onClick={() => setSshKey(undefined)}
>
<span class="material-icons">close</span>
</button>
</div>
<span class="justify-self-end font-light">has hw spec</span>
<span>{data().has_hw_specs ? "Yes" : "Not yet"}</span>
<div class="col-span-2 justify-self-center">
<button
class="btn btn-primary join-item btn-sm"
onClick={async () => {
const curr_uri = activeURI();
if (!curr_uri) {
return;
}
if (!query.data?.machine.deploy.targetHost) {
return;
}
const lt = toast.loading("Generating HW spec ...");
const response = await callApi(
"generate_machine_hardware_info",
{
machine_name: params.id,
clan_dir: curr_uri,
hostname: query.data.machine.deploy.targetHost,
},
);
toast.dismiss(lt);
if (response.status === "success") {
toast.success("HW specification processed successfully");
}
if (response.status === "error") {
toast.error(
"Failed to generate. " + response.errors[0].message,
);
}
query.refetch();
}}
>
Generate HW Spec
</button>
</div>
</div>
)}
</Show>
</div>
);
}; };

View File

@@ -2,6 +2,7 @@ import { callApi, OperationArgs } from "@/src/api";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/components/TextInput";
import { createForm, required, reset } from "@modular-forms/solid"; import { createForm, required, reset } from "@modular-forms/solid";
import { useNavigate } from "@solidjs/router";
import { createQuery, useQueryClient } from "@tanstack/solid-query"; import { createQuery, useQueryClient } from "@tanstack/solid-query";
import { Match, Switch } from "solid-js"; import { Match, Switch } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
@@ -9,6 +10,7 @@ import toast from "solid-toast";
type CreateMachineForm = OperationArgs<"create_machine">; type CreateMachineForm = OperationArgs<"create_machine">;
export function CreateMachine() { export function CreateMachine() {
const navigate = useNavigate();
const [formStore, { Form, Field }] = createForm<CreateMachineForm>({ const [formStore, { Form, Field }] = createForm<CreateMachineForm>({
initialValues: { initialValues: {
flake: { flake: {
@@ -49,7 +51,7 @@ export function CreateMachine() {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [activeURI(), "list_machines"], queryKey: [activeURI(), "list_machines"],
}); });
// setRoute("machines"); navigate("/machines");
} else { } else {
toast.error( toast.error(
`Error: ${response.errors[0].message}. Machine ${values.machine.name} could not be created`, `Error: ${response.errors[0].message}. Machine ${values.machine.name} could not be created`,

View File

@@ -37,21 +37,4 @@ describe.concurrent("API types work properly", () => {
| { status: "error"; errors: any } | { status: "error"; errors: any }
>(); >();
}); });
it("Machine show receives an object with at least: machine_name, machine_description and machine_icon", async () => {
expectTypeOf(pyApi.show_machine.receive)
.parameter(0)
.parameter(0)
.toMatchTypeOf<
| {
status: "success";
data: {
machine_name: string;
machine_icon?: string | null;
machine_description?: string | null;
};
}
| { status: "error"; errors: any }
>();
});
}); });