Can enable autostart of VMs

This commit is contained in:
Pierre HUBERT 2023-10-28 17:30:27 +02:00
parent 9a15fb4f60
commit 335aec788e
10 changed files with 304 additions and 56 deletions

View File

@ -308,3 +308,39 @@ impl Handler<ScreenshotDomainReq> for LibVirtActor {
Ok(png_out.into_inner()) Ok(png_out.into_inner())
} }
} }
#[derive(Message)]
#[rtype(result = "anyhow::Result<bool>")]
pub struct IsDomainAutostart(pub DomainXMLUuid);
impl Handler<IsDomainAutostart> for LibVirtActor {
type Result = anyhow::Result<bool>;
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<SetDomainAutostart> 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(())
}
}

View File

@ -90,6 +90,28 @@ pub async fn update(
Ok(HttpResponse::Ok().finish()) 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<SingleVMUUidReq>) -> 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<SingleVMUUidReq>,
body: web::Json<VMAutostart>,
) -> HttpResult {
client.set_domain_autostart(id.uid, body.autostart).await?;
Ok(HttpResponse::Accepted().json("OK"))
}
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct DeleteVMQuery { pub struct DeleteVMQuery {
keep_files: bool, keep_files: bool,

View File

@ -79,4 +79,19 @@ impl LibVirtClient {
pub async fn screenshot_domain(&self, id: DomainXMLUuid) -> anyhow::Result<Vec<u8>> { pub async fn screenshot_domain(&self, id: DomainXMLUuid) -> anyhow::Result<Vec<u8>> {
self.0.send(libvirt_actor::ScreenshotDomainReq(id)).await? 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<bool> {
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?
}
} }

View File

@ -77,7 +77,6 @@ pub struct VMInfo {
pub iso_file: Option<String>, pub iso_file: Option<String>,
/// 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 /// 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<Disk>, pub disks: Vec<Disk>,
// TODO : autostart
// TODO : network interface // TODO : network interface
} }

View File

@ -144,6 +144,14 @@ async fn main() -> std::io::Result<()> {
.route("/api/vm/create", web::post().to(vm_controller::create)) .route("/api/vm/create", web::post().to(vm_controller::create))
.route("/api/vm/list", web::get().to(vm_controller::list_all)) .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}", 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::put().to(vm_controller::update))
.route("/api/vm/{uid}", web::delete().to(vm_controller::delete)) .route("/api/vm/{uid}", web::delete().to(vm_controller::delete))
.route("/api/vm/{uid}/start", web::get().to(vm_controller::start)) .route("/api/vm/{uid}/start", web::get().to(vm_controller::start))

View File

@ -154,6 +154,29 @@ export class VMApi {
return new VMInfo(data); return new VMInfo(data);
} }
/**
* Check if autostart is enabled on a VM
*/
static async IsAutostart(vm: VMInfo): Promise<boolean> {
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<void> {
await APIClient.exec({
uri: `/vm/${vm.uuid}/autostart`,
method: "PUT",
jsonData: { autostart: enabled },
});
}
/** /**
* Get the state of a VM * Get the state of a VM
*/ */

View File

@ -1,5 +1,5 @@
import { Alert, Box, Button, CircularProgress } from "@mui/material"; import { Alert, Box, Button, CircularProgress } from "@mui/material";
import { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
enum State { enum State {
Loading, Loading,
@ -13,6 +13,8 @@ export function AsyncWidget(p: {
errMsg: string; errMsg: string;
build: () => React.ReactElement; build: () => React.ReactElement;
ready?: boolean; ready?: boolean;
buildLoading?: () => React.ReactElement;
buildError?: (e: string) => React.ReactElement;
errAdditionalElement?: () => React.ReactElement; errAdditionalElement?: () => React.ReactElement;
}): React.ReactElement { }): React.ReactElement {
const [state, setState] = useState(State.Loading); const [state, setState] = useState(State.Loading);
@ -39,6 +41,7 @@ export function AsyncWidget(p: {
if (state === State.Error) if (state === State.Error)
return ( return (
p.buildError?.(p.errMsg) ?? (
<Box <Box
component="div" component="div"
sx={{ sx={{
@ -66,10 +69,12 @@ export function AsyncWidget(p: {
{p.errAdditionalElement && p.errAdditionalElement()} {p.errAdditionalElement && p.errAdditionalElement()}
</Box> </Box>
)
); );
if (state === State.Loading || p.ready === false) if (state === State.Loading || p.ready === false)
return ( return (
p.buildLoading?.() ?? (
<Box <Box
component="div" component="div"
sx={{ sx={{
@ -86,6 +91,7 @@ export function AsyncWidget(p: {
> >
<CircularProgress /> <CircularProgress />
</Box> </Box>
)
); );
return p.build(); return p.build();

View File

@ -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<boolean | undefined>();
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 (
<AsyncWidget
loadKey={p.vm.uuid}
load={load}
errMsg="Failed to check autostart status of the VM!"
buildLoading={() => (
<Typography>
<CircularProgress size={"1rem"} /> Checking for autostart
</Typography>
)}
buildError={(e: string) => <Alert severity="error">{e}</Alert>}
build={() => (
<VMAutostartInputInner
editable={p.editable}
enabled={enabled!}
setEnabled={update}
/>
)}
/>
);
}
function VMAutostartInputInner(p: {
editable: boolean;
enabled: boolean;
setEnabled: (b: boolean) => void;
}): React.ReactElement {
return (
<div>
<CheckboxInput
editable={p.editable}
checked={p.enabled}
label="Autostart VM"
onValueChange={p.setEnabled}
/>
</div>
);
}

View File

@ -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 (
<ListItem
secondaryAction={
p.editable && (
<IconButton
edge="end"
aria-label="detach iso file"
onClick={() => {
p.onChange(undefined);
}}
>
<Tooltip title="Detach ISO file">
<DeleteIcon />
</Tooltip>
</IconButton>
)
}
>
<ListItemAvatar>
<Avatar>
<Icon path={mdiDisc} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={iso?.filename}
secondary={filesize(iso?.size ?? 0)}
/>
</ListItem>
);
}
return (
<SelectInput
label="ISO file"
editable={p.editable}
value={p.value}
onValueChange={p.onChange}
options={[
{ label: "None", value: undefined },
...p.isoList.map((i) => {
return {
label: `${i.filename} ${filesize(i.size)}`,
value: i.filename,
};
}),
]}
/>
);
}

View File

@ -12,6 +12,8 @@ import { AsyncWidget } from "../AsyncWidget";
import React from "react"; import React from "react";
import { filesize } from "filesize"; import { filesize } from "filesize";
import { VMDisksList } from "../forms/VMDisksList"; import { VMDisksList } from "../forms/VMDisksList";
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
import { VMAutostartInput } from "../forms/VMAutostartInput";
interface DetailsProps { interface DetailsProps {
vm: VMInfo; vm: VMInfo;
@ -32,13 +34,13 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
loadKey={"1"} loadKey={"1"}
load={load} load={load}
errMsg="Failed to load the list of ISO files" errMsg="Failed to load the list of ISO files"
build={() => <VMDetailsInner iso={list!} {...p} />} build={() => <VMDetailsInner isoList={list!} {...p} />}
/> />
); );
} }
function VMDetailsInner( function VMDetailsInner(
p: DetailsProps & { iso: IsoFile[] } p: DetailsProps & { isoList: IsoFile[] }
): React.ReactElement { ): React.ReactElement {
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
@ -154,27 +156,20 @@ function VMDetailsInner(
p.onChange?.(); p.onChange?.();
}} }}
/> />
{p.vm.uuid && <VMAutostartInput editable={p.editable} vm={p.vm} />}
</EditSection> </EditSection>
{/* Storage section */} {/* Storage section */}
<EditSection title="Storage"> <EditSection title="Storage">
<SelectInput <VMSelectIsoInput
label="ISO file"
editable={p.editable} editable={p.editable}
isoList={p.isoList}
value={p.vm.iso_file} value={p.vm.iso_file}
onValueChange={(v) => { onChange={(v) => {
p.vm.iso_file = v; p.vm.iso_file = v;
p.onChange?.(); p.onChange?.();
}} }}
options={[
{ label: "None", value: undefined },
...p.iso.map((i) => {
return {
label: `${i.filename} ${filesize(i.size)}`,
value: i.filename,
};
}),
]}
/> />
<VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} /> <VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} />
</EditSection> </EditSection>