diff --git a/pkgs/clan-cli/clan_cli/api/modules.py b/pkgs/clan-cli/clan_cli/api/modules.py index 0812c89c5..78b6beb63 100644 --- a/pkgs/clan-cli/clan_cli/api/modules.py +++ b/pkgs/clan-cli/clan_cli/api/modules.py @@ -162,6 +162,11 @@ def get_inventory(base_path: str) -> Inventory: def set_service_instance( base_path: str, module_name: str, instance_name: str, config: dict[str, Any] ) -> None: + """ + A function that allows to set any service instance in the inventory. + Takes any untyped dict. The dict is then checked and converted into the correct type using the type hints of the service. + If any conversion error occurs, the function will raise an error. + """ service_keys = get_type_hints(Service).keys() if module_name not in service_keys: diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index 139ab9bde..bc754d661 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -3,12 +3,13 @@ import json import logging from dataclasses import dataclass from pathlib import Path +from typing import Literal from clan_cli.api import API from clan_cli.cmd import run_no_stdout -from clan_cli.errors import ClanError +from clan_cli.errors import ClanCmdError, ClanError from clan_cli.inventory import Machine, load_inventory_eval -from clan_cli.nix import nix_eval +from clan_cli.nix import nix_eval, nix_shell log = logging.getLogger(__name__) @@ -66,6 +67,52 @@ def list_nixos_machines(flake_url: str | Path) -> list[str]: raise ClanError(f"Error decoding machines from flake: {e}") +@dataclass +class ConnectionOptions: + keyfile: str | None = None + timeout: int = 2 + + +@API.register +def check_machine_online( + flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None +) -> Literal["Online", "Offline"]: + machine = load_inventory_eval(flake_url).machines.get(machine_name) + if not machine: + raise ClanError(f"Machine {machine_name} not found in inventory") + + hostname = machine.deploy.targetHost + + if not hostname: + raise ClanError(f"Machine {machine_name} does not specify a targetHost") + + timeout = opts.timeout if opts and opts.timeout else 2 + + cmd = nix_shell( + ["nixpkgs#util-linux", *(["nixpkgs#openssh"] if hostname else [])], + [ + "ssh", + *(["-i", f"{opts.keyfile}"] if opts and opts.keyfile else []), + # Disable strict host key checking + "-o StrictHostKeyChecking=no", + # Disable known hosts file + "-o UserKnownHostsFile=/dev/null", + f"-o ConnectTimeout={timeout}", + f"{hostname}", + "true", + "&> /dev/null", + ], + ) + try: + proc = run_no_stdout(cmd) + if proc.returncode != 0: + return "Offline" + + return "Online" + except ClanCmdError: + return "Offline" + + def list_command(args: argparse.Namespace) -> None: flake_path = args.flake.path for name in list_nixos_machines(flake_path): diff --git a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx index 31b035f32..ab773ee9a 100644 --- a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx +++ b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx @@ -11,7 +11,7 @@ export const BlockDevicesView: Component = () => { } = createQuery(() => ({ queryKey: ["block_devices"], queryFn: async () => { - const result = await callApi("show_block_devices", {}); + const result = await callApi("show_block_devices", { options: {} }); if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 309e00cd0..91f7a1f06 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -45,7 +45,7 @@ export const Flash = () => { /* ==== WIFI NETWORK ==== */ const [wifiNetworks, setWifiNetworks] = createSignal([]); const [passwordVisibility, setPasswordVisibility] = createSignal( - [], + [] ); createEffect(() => { @@ -67,7 +67,7 @@ export const Flash = () => { const updatedNetworks = wifiNetworks().filter((_, i) => i !== index); setWifiNetworks(updatedNetworks); const updatedVisibility = passwordVisibility().filter( - (_, i) => i !== index, + (_, i) => i !== index ); setPasswordVisibility(updatedVisibility); setValue(formStore, "wifi", updatedNetworks); @@ -83,7 +83,9 @@ export const Flash = () => { const deviceQuery = createQuery(() => ({ queryKey: ["block_devices"], queryFn: async () => { - const result = await callApi("show_block_devices", { options: {} }); + const result = await callApi("show_block_devices", { + options: {}, + }); if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; }, diff --git a/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx b/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx index e7a9e2fde..aa022edb5 100644 --- a/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx @@ -1,14 +1,25 @@ import { callApi } from "@/src/api"; import { activeURI } from "@/src/App"; +import { SelectInput } from "@/src/components/SelectInput"; +import { createForm } from "@modular-forms/solid"; import { useParams } from "@solidjs/router"; import { createQuery } from "@tanstack/solid-query"; -import { createSignal, Show } from "solid-js"; +import { createSignal, For, Show } from "solid-js"; import toast from "solid-toast"; +type InstallForm = { + disk: string; +}; + export const MachineDetails = () => { const params = useParams(); const query = createQuery(() => ({ - queryKey: [activeURI(), "machine", params.id], + queryKey: [ + activeURI(), + "machine", + params.id, + "get_inventory_machine_details", + ], queryFn: async () => { const curr = activeURI(); if (curr) { @@ -24,12 +35,66 @@ export const MachineDetails = () => { const [sshKey, setSshKey] = createSignal(); + const [formStore, { Form, Field }] = createForm({}); + const handleSubmit = async (values: InstallForm) => { + return null; + }; + + const targetHost = () => query?.data?.machine.deploy.targetHost; + const remoteDiskQuery = createQuery(() => ({ + queryKey: [activeURI(), "machine", targetHost(), "show_block_devices"], + queryFn: async () => { + const curr = activeURI(); + if (curr) { + const result = await callApi("show_block_devices", { + options: { + hostname: targetHost(), + keyfile: sshKey(), + }, + }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + } + }, + })); + + const onlineStatusQuery = createQuery(() => ({ + queryKey: [activeURI(), "machine", targetHost(), "check_machine_online"], + queryFn: async () => { + const curr = activeURI(); + if (curr) { + const result = await callApi("check_machine_online", { + flake_url: curr, + machine_name: params.id, + opts: { + keyfile: sshKey(), + }, + }); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + } + }, + refetchInterval: 5000, + })); + return (
{query.isLoading && } {(data) => (
+ }> + + + + {onlineStatusQuery.data} + + {data().machine.name} description @@ -65,6 +130,7 @@ export const MachineDetails = () => { has hw spec {data().has_hw_specs ? "Yes" : "Not yet"} +
+ +
+ + {(field, props) => ( + + {(disks) => ( + <> + + + {(dev) => ( + + )} + + + )} + + } + /> + )} + +
)}