Clan-app: generate hw spec via ssh
This commit is contained in:
@@ -114,7 +114,7 @@ def generate_machine_hardware_info(
|
||||
"-o StrictHostKeyChecking=no",
|
||||
# Disable known hosts file
|
||||
"-o UserKnownHostsFile=/dev/null",
|
||||
f"root@{hostname}",
|
||||
f"{hostname}",
|
||||
"nixos-generate-config",
|
||||
# Filesystems are managed by disko
|
||||
"--no-filesystems",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from clan_cli.api import API
|
||||
@@ -18,6 +19,33 @@ def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
||||
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
|
||||
def list_nixos_machines(flake_url: str | Path) -> list[str]:
|
||||
cmd = nix_eval(
|
||||
|
||||
@@ -21,10 +21,6 @@ def test_machine_subcommands(
|
||||
assert "vm2" in out.out
|
||||
|
||||
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)
|
||||
|
||||
cli.run(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { callApi, SuccessData } from "../api";
|
||||
import { Menu } from "./Menu";
|
||||
import { activeURI } from "../App";
|
||||
import toast from "solid-toast";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
type MachineDetails = SuccessData<"list_inventory_machines">["data"][string];
|
||||
|
||||
@@ -16,7 +17,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
const { name, info, nixOnly } = props;
|
||||
|
||||
const [deploying, setDeploying] = createSignal<boolean>(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<li>
|
||||
<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">
|
||||
<li>
|
||||
<a
|
||||
// onClick={() => {
|
||||
// setRoute("machines/edit");
|
||||
// }}
|
||||
onClick={() => {
|
||||
navigate("/machines/" + name);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
Details
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
|
||||
@@ -3,10 +3,15 @@ import { Header } from "./header";
|
||||
import { Sidebar } from "../Sidebar";
|
||||
import { activeURI, clanList } from "../App";
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { Toaster } from "solid-toast";
|
||||
|
||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
createEffect(() => {
|
||||
console.log("Layout props", props.location);
|
||||
});
|
||||
return (
|
||||
<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 ">
|
||||
<input
|
||||
id="toplevel-drawer"
|
||||
|
||||
@@ -1,6 +1,110 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
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 = () => {
|
||||
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 { TextInput } from "@/src/components/TextInput";
|
||||
import { createForm, required, reset } from "@modular-forms/solid";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { Match, Switch } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
@@ -9,6 +10,7 @@ import toast from "solid-toast";
|
||||
type CreateMachineForm = OperationArgs<"create_machine">;
|
||||
|
||||
export function CreateMachine() {
|
||||
const navigate = useNavigate();
|
||||
const [formStore, { Form, Field }] = createForm<CreateMachineForm>({
|
||||
initialValues: {
|
||||
flake: {
|
||||
@@ -49,7 +51,7 @@ export function CreateMachine() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [activeURI(), "list_machines"],
|
||||
});
|
||||
// setRoute("machines");
|
||||
navigate("/machines");
|
||||
} else {
|
||||
toast.error(
|
||||
`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 }
|
||||
>();
|
||||
});
|
||||
|
||||
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