Can enable autostart of VMs
This commit is contained in:
parent
9a15fb4f60
commit
335aec788e
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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,53 +41,57 @@ export function AsyncWidget(p: {
|
|||||||
|
|
||||||
if (state === State.Error)
|
if (state === State.Error)
|
||||||
return (
|
return (
|
||||||
<Box
|
p.buildError?.(p.errMsg) ?? (
|
||||||
component="div"
|
<Box
|
||||||
sx={{
|
component="div"
|
||||||
display: "flex",
|
sx={{
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
height: "100%",
|
alignItems: "center",
|
||||||
flex: "1",
|
height: "100%",
|
||||||
flexDirection: "column",
|
flex: "1",
|
||||||
backgroundColor: (theme) =>
|
flexDirection: "column",
|
||||||
theme.palette.mode === "light"
|
backgroundColor: (theme) =>
|
||||||
? theme.palette.grey[100]
|
theme.palette.mode === "light"
|
||||||
: theme.palette.grey[900],
|
? theme.palette.grey[100]
|
||||||
}}
|
: theme.palette.grey[900],
|
||||||
>
|
}}
|
||||||
<Alert
|
|
||||||
variant="outlined"
|
|
||||||
severity="error"
|
|
||||||
style={{ margin: "0px 15px 15px 15px" }}
|
|
||||||
>
|
>
|
||||||
{p.errMsg}
|
<Alert
|
||||||
</Alert>
|
variant="outlined"
|
||||||
|
severity="error"
|
||||||
|
style={{ margin: "0px 15px 15px 15px" }}
|
||||||
|
>
|
||||||
|
{p.errMsg}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<Button onClick={load}>Try again</Button>
|
<Button onClick={load}>Try again</Button>
|
||||||
|
|
||||||
{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 (
|
||||||
<Box
|
p.buildLoading?.() ?? (
|
||||||
component="div"
|
<Box
|
||||||
sx={{
|
component="div"
|
||||||
display: "flex",
|
sx={{
|
||||||
justifyContent: "center",
|
display: "flex",
|
||||||
alignItems: "center",
|
justifyContent: "center",
|
||||||
height: "100%",
|
alignItems: "center",
|
||||||
flex: "1",
|
height: "100%",
|
||||||
backgroundColor: (theme) =>
|
flex: "1",
|
||||||
theme.palette.mode === "light"
|
backgroundColor: (theme) =>
|
||||||
? theme.palette.grey[100]
|
theme.palette.mode === "light"
|
||||||
: theme.palette.grey[900],
|
? theme.palette.grey[100]
|
||||||
}}
|
: theme.palette.grey[900],
|
||||||
>
|
}}
|
||||||
<CircularProgress />
|
>
|
||||||
</Box>
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return p.build();
|
return p.build();
|
||||||
|
70
virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx
Normal file
70
virtweb_frontend/src/widgets/forms/VMAutostartInput.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
74
virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx
Normal file
74
virtweb_frontend/src/widgets/forms/VMSelectIsoInput.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user