1 Commits

Author SHA1 Message Date
bae22e9daf Update dependency eslint-plugin-react-hooks to ^5.2.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-05-31 00:09:43 +00:00
16 changed files with 101 additions and 377 deletions

View File

@ -189,46 +189,6 @@ pub async fn handle_convert_request(
Ok(HttpResponse::Accepted().json("Successfully converted disk file"))
}
#[derive(serde::Deserialize)]
pub struct RenameDiskImageRequest {
name: String,
}
/// Rename disk image
pub async fn rename(
p: web::Path<DiskFilePath>,
req: web::Json<RenameDiskImageRequest>,
) -> HttpResult {
// Check source
if !files_utils::check_file_name(&p.filename) {
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
}
let src_path = AppConfig::get().disk_images_file_path(&p.filename);
if !src_path.exists() {
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
}
// Check destination
if !files_utils::check_file_name(&req.name) {
return Ok(HttpResponse::BadRequest().json("Invalid dst file name!"));
}
let dst_path = AppConfig::get().disk_images_file_path(&req.name);
if dst_path.exists() {
return Ok(HttpResponse::Conflict().json("Destination name already exists!"));
}
// Check extension
let disk = DiskFileInfo::load_file(&src_path)?;
if !disk.format.ext().iter().any(|e| req.name.ends_with(e)) {
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
}
// Perform rename
std::fs::rename(&src_path, &dst_path)?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete a disk image
pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
if !files_utils::check_file_name(&p.filename) {

View File

@ -22,13 +22,10 @@ pub struct DomainMetadataXML {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "os")]
pub struct OSXML {
#[serde(rename = "@firmware", default, skip_serializing_if = "Option::is_none")]
pub firmware: Option<String>,
#[serde(rename = "@firmware", default)]
pub firmware: String,
pub r#type: OSTypeXML,
#[serde(skip_serializing_if = "Option::is_none")]
pub loader: Option<OSLoaderXML>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bootmenu: Option<OSBootMenuXML>,
pub smbios: Option<OSSMBiosXML>,
}
@ -52,16 +49,6 @@ pub struct OSLoaderXML {
pub secure: String,
}
/// Legacy boot menu information
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "bootmenu")]
pub struct OSBootMenuXML {
#[serde(rename = "@enable")]
pub enable: String,
#[serde(rename = "@timeout")]
pub timeout: usize,
}
/// SMBIOS System information
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename = "smbios")]

View File

@ -6,7 +6,7 @@ use crate::libvirt_rest_structures::LibVirtStructError;
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
use crate::utils::file_size_utils::FileSize;
use crate::utils::files_utils;
use crate::utils::vm_file_disks_utils::{VMDiskBus, VMDiskFormat, VMFileDisk};
use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk};
use lazy_regex::regex;
use num::Integer;
@ -17,7 +17,6 @@ pub struct VMGroupId(pub String);
#[derive(serde::Serialize, serde::Deserialize)]
pub enum BootType {
Legacy,
UEFI,
UEFISecureBoot,
}
@ -314,11 +313,7 @@ impl VMInfo {
"vd{}",
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
),
bus: match disk.bus {
VMDiskBus::Virtio => "virtio",
VMDiskBus::SATA => "sata",
}
.to_string(),
bus: "virtio".to_string(),
},
readonly: None,
boot: DiskBootXML {
@ -352,26 +347,13 @@ impl VMInfo {
machine: "q35".to_string(),
body: "hvm".to_string(),
},
firmware: match self.boot_type {
BootType::Legacy => None,
_ => Some("efi".to_string()),
},
loader: match self.boot_type {
BootType::Legacy => None,
_ => Some(OSLoaderXML {
secure: match self.boot_type {
BootType::UEFISecureBoot => "yes".to_string(),
_ => "no".to_string(),
},
}),
},
bootmenu: match self.boot_type {
BootType::Legacy => Some(OSBootMenuXML {
enable: "yes".to_string(),
timeout: 3000,
}),
_ => None,
},
firmware: "efi".to_string(),
loader: Some(OSLoaderXML {
secure: match self.boot_type {
BootType::UEFI => "no".to_string(),
BootType::UEFISecureBoot => "yes".to_string(),
},
}),
smbios: Some(OSSMBiosXML {
mode: "sysinfo".to_string(),
}),
@ -463,10 +445,9 @@ impl VMInfo {
.virtweb
.group
.map(VMGroupId),
boot_type: match (domain.os.loader, domain.os.bootmenu) {
(_, Some(_)) => BootType::Legacy,
(None, _) => BootType::UEFI,
(Some(l), _) => match l.secure.as_str() {
boot_type: match domain.os.loader {
None => BootType::UEFI,
Some(l) => match l.secure.as_str() {
"yes" => BootType::UEFISecureBoot,
_ => BootType::UEFI,
},
@ -498,7 +479,7 @@ impl VMInfo {
.iter()
.filter(|d| d.device == "disk")
.map(|d| {
VMFileDisk::load_from_file(&d.source.file, &d.target.bus)
VMFileDisk::load_from_file(&d.source.file)
.expect("Failed to load file disk information!")
})
.collect(),

View File

@ -352,10 +352,6 @@ async fn main() -> std::io::Result<()> {
"/api/disk_images/{filename}/convert",
web::post().to(disk_images_controller::convert),
)
.route(
"/api/disk_images/{filename}/rename",
web::post().to(disk_images_controller::rename),
)
.route(
"/api/disk_images/{filename}",
web::delete().to(disk_images_controller::delete),

View File

@ -183,13 +183,7 @@ impl DiskFileInfo {
// Convert QCow2 to Raw file
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::Raw { is_sparse }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert")
.arg("-f")
.arg("qcow2")
.arg("-O")
.arg("raw")
.arg(&self.file_path)
.arg(&temp_file);
cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
if !is_sparse {
cmd.args(["-S", "0"]);
@ -203,8 +197,6 @@ impl DiskFileInfo {
(DiskFileFormat::QCow2 { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert")
.arg("-f")
.arg("qcow2")
.arg("-O")
.arg("qcow2")
.arg(&self.file_path)
@ -215,13 +207,7 @@ impl DiskFileInfo {
// Convert Raw to QCow2 file
(DiskFileFormat::Raw { .. }, DiskFileFormat::QCow2 { .. }) => {
let mut cmd = Command::new(constants::QEMU_IMAGE_PROGRAM);
cmd.arg("convert")
.arg("-f")
.arg("raw")
.arg("-O")
.arg("qcow2")
.arg(&self.file_path)
.arg(&temp_file);
cmd.arg("convert").arg(&self.file_path).arg(&temp_file);
cmd
}

View File

@ -13,12 +13,6 @@ enum VMDisksError {
Config(&'static str),
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum VMDiskBus {
Virtio,
SATA,
}
/// Disk allocation type
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "format")]
@ -36,8 +30,6 @@ pub struct VMFileDisk {
pub name: String,
/// Disk size, in bytes
pub size: FileSize,
/// Disk bus
pub bus: VMDiskBus,
/// Disk format
#[serde(flatten)]
pub format: VMDiskFormat,
@ -49,7 +41,7 @@ pub struct VMFileDisk {
}
impl VMFileDisk {
pub fn load_from_file(path: &str, bus: &str) -> anyhow::Result<Self> {
pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
let file = Path::new(path);
let info = DiskFileInfo::load_file(file)?;
@ -69,13 +61,6 @@ impl VMFileDisk {
DiskFileFormat::QCow2 { .. } => VMDiskFormat::QCow2,
_ => anyhow::bail!("Unsupported image format: {:?}", info.format),
},
bus: match bus {
"virtio" => VMDiskBus::Virtio,
"sata" => VMDiskBus::SATA,
_ => anyhow::bail!("Unsupported disk bus type: {bus}"),
},
delete: false,
from_image: None,
})

View File

@ -94,17 +94,6 @@ export class DiskImageApi {
});
}
/**
* Rename disk image file
*/
static async Rename(file: DiskImage, name: string): Promise<void> {
await APIClient.exec({
method: "POST",
uri: `/disk_images/${file.file_name}/rename`,
jsonData: { name },
});
}
/**
* Delete disk image file
*/

View File

@ -19,13 +19,9 @@ export type VMState =
export type VMFileDisk = BaseFileVMDisk & (RawVMDisk | QCow2Disk);
export type DiskBusType = "Virtio" | "SATA";
export interface BaseFileVMDisk {
size: number;
name: string;
bus: DiskBusType;
delete: boolean;
// For new disk only
@ -82,8 +78,6 @@ export interface VMNetBridge {
bridge: string;
}
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
interface VMInfoInterface {
name: string;
uuid?: string;
@ -91,7 +85,7 @@ interface VMInfoInterface {
title?: string;
description?: string;
group?: string;
boot_type: VMBootType;
boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64";
memory: number;
number_vcpu: number;
@ -110,7 +104,7 @@ export class VMInfo implements VMInfoInterface {
title?: string;
description?: string;
group?: string;
boot_type: VMBootType;
boot_type: "UEFI" | "UEFISecureBoot";
architecture: "i686" | "x86_64";
number_vcpu: number;
memory: number;

View File

@ -1,7 +1,6 @@
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
import LoopIcon from "@mui/icons-material/Loop";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
Alert,
@ -9,10 +8,6 @@ import {
CircularProgress,
IconButton,
LinearProgress,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip,
Typography,
} from "@mui/material";
@ -169,11 +164,15 @@ function DiskImageList(p: {
const confirm = useConfirm();
const loadingMessage = useLoadingMessage();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
const [currConversion, setCurrConversion] = React.useState<
DiskImage | undefined
>();
const [dlProgress, setDlProgress] = React.useState<undefined | number>();
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Download disk image file
const downloadDiskImage = async (entry: DiskImage) => {
@ -191,11 +190,6 @@ function DiskImageList(p: {
setDlProgress(undefined);
};
// Convert disk image file
const convertDiskImage = (entry: DiskImage) => {
setCurrConversion(entry);
};
// Delete disk image
const deleteDiskImage = async (entry: DiskImage) => {
if (
@ -227,7 +221,7 @@ function DiskImageList(p: {
);
const columns: GridColDef<(typeof p.list)[number]>[] = [
{ field: "file_name", headerName: "File name", flex: 3, editable: true },
{ field: "file_name", headerName: "File name", flex: 3 },
{
field: "format",
headerName: "Format",
@ -266,21 +260,28 @@ function DiskImageList(p: {
},
{
field: "actions",
type: "actions",
headerName: "",
width: 55,
cellClassName: "actions",
editable: false,
getActions: (params) => {
return [
<DiskImageActionMenu
key="menu"
diskImage={params.row}
onDownload={downloadDiskImage}
onConvert={convertDiskImage}
onDelete={deleteDiskImage}
/>,
];
width: 140,
renderCell(params) {
return (
<>
<Tooltip title="Convert disk image">
<IconButton onClick={() => { convertDiskImage(params.row); }}>
<LoopIcon />
</IconButton>
</Tooltip>
<Tooltip title="Download disk image">
<IconButton onClick={() => downloadDiskImage(params.row)}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete disk image">
<IconButton onClick={() => deleteDiskImage(params.row)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
);
},
},
];
@ -326,92 +327,7 @@ function DiskImageList(p: {
)}
{/* The table itself */}
<DataGrid<DiskImage>
getRowId={(c) => c.file_name}
rows={p.list}
columns={columns}
processRowUpdate={async (n, o) => {
try {
await DiskImageApi.Rename(o, n.file_name);
return n;
} catch (e) {
console.error("Failed to rename disk image!", e);
alert(`Failed to rename disk image! ${e}`);
throw e;
} finally {
p.onReload();
}
}}
/>
</>
);
}
function DiskImageActionMenu(p: {
diskImage: DiskImage;
onDownload: (d: DiskImage) => void;
onConvert: (d: DiskImage) => void;
onDelete: (d: DiskImage) => void;
}): React.ReactElement {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<>
<IconButton
aria-label="Actions"
aria-haspopup="true"
onClick={handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
{/* Download disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDownload(p.diskImage);
}}
>
<ListItemIcon>
<DownloadIcon />
</ListItemIcon>
<ListItemText secondary={"Download disk image"}>
Download
</ListItemText>
</MenuItem>
{/* Convert disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onConvert(p.diskImage);
}}
>
<ListItemIcon>
<LoopIcon />
</ListItemIcon>
<ListItemText secondary={"Convert disk image"}>Convert</ListItemText>
</MenuItem>
{/* Delete disk image */}
<MenuItem
onClick={() => {
handleClose();
p.onDelete(p.diskImage);
}}
>
<ListItemIcon>
<DeleteIcon color="error" />
</ListItemIcon>
<ListItemText secondary={"Delete disk image"}>Delete</ListItemText>
</MenuItem>
</Menu>
<DataGrid getRowId={(c) => c.file_name} rows={p.list} columns={columns} />
</>
);
}

View File

@ -1,24 +0,0 @@
import { DiskBusType } from "../../api/VMApi";
import { SelectInput } from "./SelectInput";
export function DiskBusSelect(p: {
editable: boolean;
value: DiskBusType;
label?: string;
onValueChange: (value: DiskBusType) => void;
size?: "medium" | "small";
disableUnderline?: boolean;
disableBottomMargin?: boolean;
}): React.ReactElement {
return (
<SelectInput
{...p}
label={p.label ?? "Disk bus type"}
options={[
{ label: "virtio", value: "Virtio" },
{ label: "sata", value: "SATA" },
]}
onValueChange={(v) => { p.onValueChange(v as any); }}
/>
);
}

View File

@ -1,12 +1,12 @@
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import {
FormControl,
InputLabel,
MenuItem,
Select,
MenuItem,
SelectChangeEvent,
} from "@mui/material";
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { FileDiskImageWidget } from "../FileDiskImageWidget";
/**
@ -30,7 +30,7 @@ export function DiskImageSelect(p: {
<i>None</i>
</MenuItem>
{p.list.map((d) => (
<MenuItem key={d.file_name} value={d.file_name}>
<MenuItem value={d.file_name}>
<FileDiskImageWidget image={d} />
</MenuItem>
))}

View File

@ -17,11 +17,8 @@ export function SelectInput(p: {
value?: string;
editable: boolean;
label?: string;
size?: "medium" | "small";
options: SelectOption[];
onValueChange: (o?: string) => void;
disableUnderline?: boolean;
disableBottomMargin?: boolean;
}): React.ReactElement {
if (!p.editable && !p.value) return <></>;
@ -31,18 +28,12 @@ export function SelectInput(p: {
}
return (
<FormControl
fullWidth
variant="standard"
style={{ marginBottom: p.disableBottomMargin ? "0px" : "15px" }}
>
<FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
{p.label && <InputLabel>{p.label}</InputLabel>}
<Select
{...p}
value={p.value ?? ""}
onChange={(e) => {
p.onValueChange(e.target.value);
}}
label={p.label}
onChange={(e) => { p.onValueChange(e.target.value); }}
>
{p.options.map((e) => (
<MenuItem

View File

@ -1,20 +1,28 @@
import { mdiHarddiskPlus } from "@mdi/js";
import { mdiHarddisk, mdiHarddiskPlus } from "@mdi/js";
import Icon from "@mdi/react";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DeleteIcon from "@mui/icons-material/Delete";
import { Button, IconButton, Paper, Tooltip } from "@mui/material";
import {
Avatar,
Button,
IconButton,
ListItem,
ListItemAvatar,
ListItemText,
Paper,
Tooltip,
} from "@mui/material";
import { filesize } from "filesize";
import React from "react";
import { DiskImage } from "../../api/DiskImageApi";
import { ServerApi } from "../../api/ServerApi";
import { VMFileDisk, VMInfo, VMState } from "../../api/VMApi";
import { ConvertDiskImageDialog } from "../../dialogs/ConvertDiskImageDialog";
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
import { VMDiskFileWidget } from "../vms/VMDiskFileWidget";
import { CheckboxInput } from "./CheckboxInput";
import { DiskBusSelect } from "./DiskBusSelect";
import { DiskImageSelect } from "./DiskImageSelect";
import { SelectInput } from "./SelectInput";
import { TextInput } from "./TextInput";
import { DiskImageSelect } from "./DiskImageSelect";
import { DiskImage } from "../../api/DiskImageApi";
export function VMDisksList(p: {
vm: VMInfo;
@ -31,7 +39,6 @@ export function VMDisksList(p: {
p.vm.file_disks.push({
format: "QCow2",
size: 10000 * 1000 * 1000,
bus: "Virtio",
delete: false,
name: `disk${p.vm.file_disks.length}`,
new: true,
@ -115,8 +122,7 @@ function DiskInfo(p: {
if (!p.editable || !p.disk.new)
return (
<VMDiskFileWidget
{...p}
<ListItem
secondaryAction={
<>
{p.editable && (
@ -150,7 +156,32 @@ function DiskInfo(p: {
)}
</>
}
/>
>
<ListItemAvatar>
<Avatar>
<Icon path={mdiHarddisk} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<>
{p.disk.name}{" "}
{p.disk.deleteType && (
<span style={{ color: "red" }}>
{p.disk.deleteType === "deletefile"
? "Remove, DELETING block file"
: "Remove, keeping block file"}
</span>
)}
</>
}
secondary={`${filesize(p.disk.size)} - ${p.disk.format}${
p.disk.format == "Raw"
? " - " + (p.disk.is_sparse ? "Sparse" : "Fixed")
: ""
}`}
/>
</ListItem>
);
return (
@ -189,17 +220,6 @@ function DiskInfo(p: {
}}
/>
{/* Bus selection */}
<DiskBusSelect
editable
value={p.disk.bus}
onValueChange={(v) => {
p.disk.bus = v;
p.onChange?.();
}}
/>
{/* Raw disk: choose sparse mode */}
{p.disk.format === "Raw" && (
<CheckboxInput
editable

View File

@ -707,11 +707,6 @@ export function TokenRightsEditor(p: {
right={{ verb: "POST", path: "/api/disk_images/*/convert" }}
label="Convert disk images"
/>
<RouteRight
{...p}
right={{ verb: "POST", path: "/api/disk_images/*/rename" }}
label="Rename disk images"
/>
<RouteRight
{...p}
right={{ verb: "DELETE", path: "/api/disk_images/*" }}

View File

@ -280,7 +280,6 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
options={[
{ label: "UEFI with Secure Boot", value: "UEFISecureBoot" },
{ label: "UEFI", value: "UEFI" },
{ label: "Legacy", value: "Legacy" },
]}
/>

View File

@ -3,69 +3,18 @@ import { Icon } from "@mdi/react";
import { Avatar, ListItem, ListItemAvatar, ListItemText } from "@mui/material";
import { filesize } from "filesize";
import { VMFileDisk } from "../../api/VMApi";
import { DiskBusSelect } from "../forms/DiskBusSelect";
export function VMDiskFileWidget(p: {
editable?: boolean;
disk: VMFileDisk;
secondaryAction?: React.ReactElement;
onChange?: () => void;
}): React.ReactElement {
const info = [filesize(p.disk.size), p.disk.format];
if (p.disk.format === "Raw") info.push(p.disk.is_sparse ? "Sparse" : "Fixed");
if (!p.editable) info.push(p.disk.bus);
export function VMDiskFileWidget(p: { disk: VMFileDisk }): React.ReactElement {
return (
<ListItem secondaryAction={p.secondaryAction}>
<ListItem>
<ListItemAvatar>
<Avatar>
<Icon path={mdiHarddisk} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<>
{p.disk.name}{" "}
{p.disk.deleteType && (
<span style={{ color: "red" }}>
{p.disk.deleteType === "deletefile"
? "Remove, DELETING block file"
: "Remove, keeping block file"}
</span>
)}
</>
}
secondary={
<div style={{ display: "flex", alignItems: "center" }}>
{p.editable ? (
<div
style={{
maxWidth: "80px",
display: "inline-block",
marginRight: "10px",
}}
>
<DiskBusSelect
onValueChange={(v) => {
p.disk.bus = v;
p.onChange?.();
}}
label=""
editable
value={p.disk.bus}
size="small"
disableUnderline
disableBottomMargin
/>
</div>
) : (
""
)}
<div style={{ height: "100%" }}>{info.join(" - ")}</div>
</div>
}
primary={p.disk.name}
secondary={`${p.disk.format} - ${filesize(p.disk.size)}`}
/>
</ListItem>
);