Clan-app: generate hw spec via ssh
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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`,
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
>();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user