From 335aec788e79c23a834b375d2f65e4517cae3cdc Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 28 Oct 2023 17:30:27 +0200 Subject: [PATCH] Can enable autostart of VMs --- virtweb_backend/src/actors/libvirt_actor.rs | 36 ++++++++ .../src/controllers/vm_controller.rs | 22 +++++ virtweb_backend/src/libvirt_client.rs | 15 ++++ .../src/libvirt_rest_structures.rs | 1 - virtweb_backend/src/main.rs | 8 ++ virtweb_frontend/src/api/VMApi.ts | 23 +++++ virtweb_frontend/src/widgets/AsyncWidget.tsx | 88 ++++++++++--------- .../src/widgets/forms/VMAutostartInput.tsx | 70 +++++++++++++++ .../src/widgets/forms/VMSelectIsoInput.tsx | 74 ++++++++++++++++ .../src/widgets/vms/VMDetails.tsx | 23 ++--- 10 files changed, 304 insertions(+), 56 deletions(-) create mode 100644 virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx create mode 100644 virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx diff --git a/virtweb_backend/src/actors/libvirt_actor.rs b/virtweb_backend/src/actors/libvirt_actor.rs index 13cf627..334cb95 100644 --- a/virtweb_backend/src/actors/libvirt_actor.rs +++ b/virtweb_backend/src/actors/libvirt_actor.rs @@ -308,3 +308,39 @@ impl Handler for LibVirtActor { Ok(png_out.into_inner()) } } + +#[derive(Message)] +#[rtype(result = "anyhow::Result")] +pub struct IsDomainAutostart(pub DomainXMLUuid); + +impl Handler for LibVirtActor { + type Result = anyhow::Result; + + fn handle(&mut self, msg: IsDomainAutostart, _ctx: &mut Self::Context) -> Self::Result { + log::debug!( + "Check if autostart is enabled for a domain: {}", + msg.0.as_string() + ); + let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + Ok(domain.get_autostart()?) + } +} + +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct SetDomainAutostart(pub DomainXMLUuid, pub bool); + +impl Handler for LibVirtActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: SetDomainAutostart, _ctx: &mut Self::Context) -> Self::Result { + log::debug!( + "Set autostart enabled={} for a domain: {}", + msg.1, + msg.0.as_string() + ); + let domain = Domain::lookup_by_uuid_string(&self.m, &msg.0.as_string())?; + domain.set_autostart(msg.1)?; + Ok(()) + } +} diff --git a/virtweb_backend/src/controllers/vm_controller.rs b/virtweb_backend/src/controllers/vm_controller.rs index fdbc906..dbf72d8 100644 --- a/virtweb_backend/src/controllers/vm_controller.rs +++ b/virtweb_backend/src/controllers/vm_controller.rs @@ -90,6 +90,28 @@ pub async fn update( Ok(HttpResponse::Ok().finish()) } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct VMAutostart { + autostart: bool, +} + +/// Get autostart value of a vm +pub async fn get_autostart(client: LibVirtReq, id: web::Path) -> HttpResult { + Ok(HttpResponse::Ok().json(VMAutostart { + autostart: client.is_domain_autostart(id.uid).await?, + })) +} + +/// Configure autostart value for a vm +pub async fn set_autostart( + client: LibVirtReq, + id: web::Path, + body: web::Json, +) -> HttpResult { + client.set_domain_autostart(id.uid, body.autostart).await?; + Ok(HttpResponse::Accepted().json("OK")) +} + #[derive(serde::Deserialize)] pub struct DeleteVMQuery { keep_files: bool, diff --git a/virtweb_backend/src/libvirt_client.rs b/virtweb_backend/src/libvirt_client.rs index 2319b17..af48500 100644 --- a/virtweb_backend/src/libvirt_client.rs +++ b/virtweb_backend/src/libvirt_client.rs @@ -79,4 +79,19 @@ impl LibVirtClient { pub async fn screenshot_domain(&self, id: DomainXMLUuid) -> anyhow::Result> { self.0.send(libvirt_actor::ScreenshotDomainReq(id)).await? } + + /// Get auto-start status of a domain + pub async fn is_domain_autostart(&self, id: DomainXMLUuid) -> anyhow::Result { + self.0.send(libvirt_actor::IsDomainAutostart(id)).await? + } + + pub async fn set_domain_autostart( + &self, + id: DomainXMLUuid, + autostart: bool, + ) -> anyhow::Result<()> { + self.0 + .send(libvirt_actor::SetDomainAutostart(id, autostart)) + .await? + } } diff --git a/virtweb_backend/src/libvirt_rest_structures.rs b/virtweb_backend/src/libvirt_rest_structures.rs index 85616f0..33140f2 100644 --- a/virtweb_backend/src/libvirt_rest_structures.rs +++ b/virtweb_backend/src/libvirt_rest_structures.rs @@ -77,7 +77,6 @@ pub struct VMInfo { pub iso_file: Option, /// Storage - https://access.redhat.com/documentation/fr-fr/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-virtualized_block_devices-adding_storage_devices_to_guests#sect-Virtualization-Adding_storage_devices_to_guests-Adding_file_based_storage_to_a_guest pub disks: Vec, - // TODO : autostart // TODO : network interface } diff --git a/virtweb_backend/src/main.rs b/virtweb_backend/src/main.rs index 7ee1627..53e0c28 100644 --- a/virtweb_backend/src/main.rs +++ b/virtweb_backend/src/main.rs @@ -144,6 +144,14 @@ async fn main() -> std::io::Result<()> { .route("/api/vm/create", web::post().to(vm_controller::create)) .route("/api/vm/list", web::get().to(vm_controller::list_all)) .route("/api/vm/{uid}", web::get().to(vm_controller::get_single)) + .route( + "/api/vm/{uid}/autostart", + web::get().to(vm_controller::get_autostart), + ) + .route( + "/api/vm/{uid}/autostart", + web::put().to(vm_controller::set_autostart), + ) .route("/api/vm/{uid}", web::put().to(vm_controller::update)) .route("/api/vm/{uid}", web::delete().to(vm_controller::delete)) .route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) diff --git a/virtweb_frontend/src/api/VMApi.ts b/virtweb_frontend/src/api/VMApi.ts index c655a2b..1039fb2 100644 --- a/virtweb_frontend/src/api/VMApi.ts +++ b/virtweb_frontend/src/api/VMApi.ts @@ -154,6 +154,29 @@ export class VMApi { return new VMInfo(data); } + /** + * Check if autostart is enabled on a VM + */ + static async IsAutostart(vm: VMInfo): Promise { + return ( + await APIClient.exec({ + uri: `/vm/${vm.uuid}/autostart`, + method: "GET", + }) + ).data.autostart; + } + + /** + * Set autostart status of a VM + */ + static async SetAutostart(vm: VMInfo, enabled: boolean): Promise { + await APIClient.exec({ + uri: `/vm/${vm.uuid}/autostart`, + method: "PUT", + jsonData: { autostart: enabled }, + }); + } + /** * Get the state of a VM */ diff --git a/virtweb_frontend/src/widgets/AsyncWidget.tsx b/virtweb_frontend/src/widgets/AsyncWidget.tsx index 87c3ca2..18d24b8 100644 --- a/virtweb_frontend/src/widgets/AsyncWidget.tsx +++ b/virtweb_frontend/src/widgets/AsyncWidget.tsx @@ -1,5 +1,5 @@ import { Alert, Box, Button, CircularProgress } from "@mui/material"; -import { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; enum State { Loading, @@ -13,6 +13,8 @@ export function AsyncWidget(p: { errMsg: string; build: () => React.ReactElement; ready?: boolean; + buildLoading?: () => React.ReactElement; + buildError?: (e: string) => React.ReactElement; errAdditionalElement?: () => React.ReactElement; }): React.ReactElement { const [state, setState] = useState(State.Loading); @@ -39,53 +41,57 @@ export function AsyncWidget(p: { if (state === State.Error) return ( - - theme.palette.mode === "light" - ? theme.palette.grey[100] - : theme.palette.grey[900], - }} - > - + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} > - {p.errMsg} - + + {p.errMsg} + - + - {p.errAdditionalElement && p.errAdditionalElement()} - + {p.errAdditionalElement && p.errAdditionalElement()} + + ) ); if (state === State.Loading || p.ready === false) return ( - - theme.palette.mode === "light" - ? theme.palette.grey[100] - : theme.palette.grey[900], - }} - > - - + p.buildLoading?.() ?? ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + + + ) ); return p.build(); diff --git a/virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx b/virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx new file mode 100644 index 0000000..5930dab --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx @@ -0,0 +1,70 @@ +import { Alert, CircularProgress, Typography } from "@mui/material"; +import { VMApi, VMInfo } from "../../api/VMApi"; +import { AsyncWidget } from "../AsyncWidget"; +import React from "react"; +import { CheckboxInput } from "./CheckboxInput"; +import { useAlert } from "../../hooks/providers/AlertDialogProvider"; +import { useSnackbar } from "../../hooks/providers/SnackbarProvider"; + +export function VMAutostartInput(p: { + editable: boolean; + vm: VMInfo; +}): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + + const [enabled, setEnabled] = React.useState(); + + const load = async () => { + setEnabled(await VMApi.IsAutostart(p.vm)); + }; + + const update = async (enabled: boolean) => { + try { + await VMApi.SetAutostart(p.vm, enabled); + snackbar("Autostart status successfully updated!"); + setEnabled(enabled); + } catch (e) { + console.error(e); + alert("Failed to update autostart status of the VM!"); + } + }; + + return ( + ( + + Checking for autostart + + )} + buildError={(e: string) => {e}} + build={() => ( + + )} + /> + ); +} + +function VMAutostartInputInner(p: { + editable: boolean; + enabled: boolean; + setEnabled: (b: boolean) => void; +}): React.ReactElement { + return ( +
+ +
+ ); +} diff --git a/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx b/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx new file mode 100644 index 0000000..7859105 --- /dev/null +++ b/virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx @@ -0,0 +1,74 @@ +import { filesize } from "filesize"; +import { IsoFile } from "../../api/IsoFilesApi"; +import { SelectInput } from "./SelectInput"; +import { + Avatar, + IconButton, + ListItem, + ListItemAvatar, + ListItemText, + Tooltip, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { mdiDisc } from "@mdi/js"; +import Icon from "@mdi/react"; + +export function VMSelectIsoInput(p: { + editable: boolean; + isoList: IsoFile[]; + value?: string; + onChange: (newVal?: string) => void; +}): React.ReactElement { + if (!p.value && !p.editable) return <>; + + if (p.value) { + const iso = p.isoList.find((d) => d.filename == p.value); + return ( + { + p.onChange(undefined); + }} + > + + + + + ) + } + > + + + + + + + + ); + } + + return ( + { + return { + label: `${i.filename} ${filesize(i.size)}`, + value: i.filename, + }; + }), + ]} + /> + ); +} diff --git a/virtweb_frontend/src/widgets/vms/VMDetails.tsx b/virtweb_frontend/src/widgets/vms/VMDetails.tsx index 6356552..33cb20b 100644 --- a/virtweb_frontend/src/widgets/vms/VMDetails.tsx +++ b/virtweb_frontend/src/widgets/vms/VMDetails.tsx @@ -12,6 +12,8 @@ import { AsyncWidget } from "../AsyncWidget"; import React from "react"; import { filesize } from "filesize"; import { VMDisksList } from "../forms/VMDisksList"; +import { VMSelectIsoInput } from "../forms/VMSelectIsoInput"; +import { VMAutostartInput } from "../forms/VMAutostartInput"; interface DetailsProps { vm: VMInfo; @@ -32,13 +34,13 @@ export function VMDetails(p: DetailsProps): React.ReactElement { loadKey={"1"} load={load} errMsg="Failed to load the list of ISO files" - build={() => } + build={() => } /> ); } function VMDetailsInner( - p: DetailsProps & { iso: IsoFile[] } + p: DetailsProps & { isoList: IsoFile[] } ): React.ReactElement { return ( @@ -154,27 +156,20 @@ function VMDetailsInner( p.onChange?.(); }} /> + + {p.vm.uuid && } {/* Storage section */} - { + onChange={(v) => { p.vm.iso_file = v; p.onChange?.(); }} - options={[ - { label: "None", value: undefined }, - ...p.iso.map((i) => { - return { - label: `${i.filename} ${filesize(i.size)}`, - value: i.filename, - }; - }), - ]} />