Can restore disk image when adding disks to virtual machine
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
@@ -255,6 +255,11 @@ impl AppConfig {
|
|||||||
self.storage_path().join("disk_images")
|
self.storage_path().join("disk_images")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the path of a disk image file
|
||||||
|
pub fn disk_images_file_path(&self, name: &str) -> PathBuf {
|
||||||
|
self.disk_images_storage_path().join(name)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get VM vnc sockets directory
|
/// Get VM vnc sockets directory
|
||||||
pub fn vnc_sockets_path(&self) -> PathBuf {
|
pub fn vnc_sockets_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("vnc")
|
self.storage_path().join("vnc")
|
||||||
|
@@ -49,7 +49,7 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a file with the same name already exists
|
// Check if a file with the same name already exists
|
||||||
let dest_path = AppConfig::get().disk_images_storage_path().join(file_name);
|
let dest_path = AppConfig::get().disk_images_file_path(&file_name);
|
||||||
if dest_path.is_file() {
|
if dest_path.is_file() {
|
||||||
return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
|
return Ok(HttpResponse::Conflict().json("A file with the same name already exists!"));
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,7 @@ pub async fn download(p: web::Path<DiskFilePath>, req: HttpRequest) -> HttpResul
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = AppConfig::get()
|
let file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
||||||
.disk_images_storage_path()
|
|
||||||
.join(&p.filename);
|
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
@@ -109,9 +107,7 @@ pub async fn convert(
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid src file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let src_file_path = AppConfig::get()
|
let src_file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
||||||
.disk_images_storage_path()
|
|
||||||
.join(&p.filename);
|
|
||||||
|
|
||||||
let src = DiskFileInfo::load_file(&src_file_path)?;
|
let src = DiskFileInfo::load_file(&src_file_path)?;
|
||||||
|
|
||||||
@@ -176,9 +172,7 @@ pub async fn handle_convert_request(
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid destination file extension!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dst_file_path = AppConfig::get()
|
let dst_file_path = AppConfig::get().disk_images_file_path(&req.dest_file_name);
|
||||||
.disk_images_storage_path()
|
|
||||||
.join(&req.dest_file_name);
|
|
||||||
|
|
||||||
if dst_file_path.exists() {
|
if dst_file_path.exists() {
|
||||||
return Ok(HttpResponse::Conflict().json("Specified destination file already exists!"));
|
return Ok(HttpResponse::Conflict().json("Specified destination file already exists!"));
|
||||||
@@ -201,9 +195,7 @@ pub async fn delete(p: web::Path<DiskFilePath>) -> HttpResult {
|
|||||||
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
return Ok(HttpResponse::BadRequest().json("Invalid file name!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path = AppConfig::get()
|
let file_path = AppConfig::get().disk_images_file_path(&p.filename);
|
||||||
.disk_images_storage_path()
|
|
||||||
.join(&p.filename);
|
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
|
||||||
|
@@ -33,6 +33,9 @@ pub struct VMFileDisk {
|
|||||||
/// Disk format
|
/// Disk format
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub format: VMDiskFormat,
|
pub format: VMDiskFormat,
|
||||||
|
/// When creating a new disk, specify the disk image template to use
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub from_image: Option<String>,
|
||||||
/// Set this variable to true to delete the disk
|
/// Set this variable to true to delete the disk
|
||||||
pub delete: bool,
|
pub delete: bool,
|
||||||
}
|
}
|
||||||
@@ -59,6 +62,7 @@ impl VMFileDisk {
|
|||||||
_ => anyhow::bail!("Unsupported image format: {:?}", info.format),
|
_ => anyhow::bail!("Unsupported image format: {:?}", info.format),
|
||||||
},
|
},
|
||||||
delete: false,
|
delete: false,
|
||||||
|
from_image: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +82,19 @@ impl VMFileDisk {
|
|||||||
return Err(VMDisksError::Config("Disk size is invalid!").into());
|
return Err(VMDisksError::Config("Disk size is invalid!").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check specified disk image template
|
||||||
|
if let Some(disk_image) = &self.from_image {
|
||||||
|
if !files_utils::check_file_name(disk_image) {
|
||||||
|
return Err(VMDisksError::Config("Disk image template name is not valid!").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !AppConfig::get().disk_images_file_path(disk_image).is_file() {
|
||||||
|
return Err(
|
||||||
|
VMDisksError::Config("Specified disk image file does not exist!").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,17 +132,27 @@ impl VMFileDisk {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create disk file
|
let format = match self.format {
|
||||||
DiskFileInfo::create(
|
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
||||||
&file,
|
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
||||||
match self.format {
|
virtual_size: self.size,
|
||||||
VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
|
|
||||||
VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
|
|
||||||
virtual_size: self.size,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
self.size,
|
};
|
||||||
)?;
|
|
||||||
|
// Create / Restore disk file
|
||||||
|
match &self.from_image {
|
||||||
|
// Create disk file
|
||||||
|
None => {
|
||||||
|
DiskFileInfo::create(&file, format, self.size)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore disk image template
|
||||||
|
Some(disk_img) => {
|
||||||
|
let src_file =
|
||||||
|
DiskFileInfo::load_file(&AppConfig::get().disk_images_file_path(disk_img))?;
|
||||||
|
src_file.convert(&file, format)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,9 @@ export interface BaseFileVMDisk {
|
|||||||
name: string;
|
name: string;
|
||||||
delete: boolean;
|
delete: boolean;
|
||||||
|
|
||||||
|
// For new disk only
|
||||||
|
from_image?: string;
|
||||||
|
|
||||||
// application attributes
|
// application attributes
|
||||||
new?: boolean;
|
new?: boolean;
|
||||||
deleteType?: "keepfile" | "deletefile";
|
deleteType?: "keepfile" | "deletefile";
|
||||||
|
40
virtweb_frontend/src/widgets/forms/DiskImageSelect.tsx
Normal file
40
virtweb_frontend/src/widgets/forms/DiskImageSelect.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { DiskImage } from "../../api/DiskImageApi";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
SelectChangeEvent,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { FileDiskImageWidget } from "../FileDiskImageWidget";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a disk image
|
||||||
|
*/
|
||||||
|
export function DiskImageSelect(p: {
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
onValueChange: (image: string | undefined) => void;
|
||||||
|
list: DiskImage[];
|
||||||
|
}): React.ReactElement {
|
||||||
|
const handleChange = (event: SelectChangeEvent) => {
|
||||||
|
p.onValueChange(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl fullWidth variant="standard">
|
||||||
|
<InputLabel>{p.label}</InputLabel>
|
||||||
|
<Select value={p.value} label={p.label} onChange={handleChange}>
|
||||||
|
<MenuItem value={undefined}>
|
||||||
|
<i>None</i>
|
||||||
|
</MenuItem>
|
||||||
|
{p.list.map((d) => (
|
||||||
|
<MenuItem value={d.file_name}>
|
||||||
|
<FileDiskImageWidget image={d} />
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@@ -17,6 +17,7 @@ export function TextInput(p: {
|
|||||||
type?: React.HTMLInputTypeAttribute;
|
type?: React.HTMLInputTypeAttribute;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.editable && (p.value ?? "") === "") return <></>;
|
if (!p.editable && (p.value ?? "") === "") return <></>;
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export function TextInput(p: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
disabled={p.disabled}
|
||||||
label={p.label}
|
label={p.label}
|
||||||
value={p.value ?? ""}
|
value={p.value ?? ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
@@ -21,12 +21,15 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
|||||||
import { CheckboxInput } from "./CheckboxInput";
|
import { CheckboxInput } from "./CheckboxInput";
|
||||||
import { SelectInput } from "./SelectInput";
|
import { SelectInput } from "./SelectInput";
|
||||||
import { TextInput } from "./TextInput";
|
import { TextInput } from "./TextInput";
|
||||||
|
import { DiskImageSelect } from "./DiskImageSelect";
|
||||||
|
import { DiskImage } from "../../api/DiskImageApi";
|
||||||
|
|
||||||
export function VMDisksList(p: {
|
export function VMDisksList(p: {
|
||||||
vm: VMInfo;
|
vm: VMInfo;
|
||||||
state?: VMState;
|
state?: VMState;
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
diskImagesList: DiskImage[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const [currBackupRequest, setCurrBackupRequest] = React.useState<
|
const [currBackupRequest, setCurrBackupRequest] = React.useState<
|
||||||
VMFileDisk | undefined
|
VMFileDisk | undefined
|
||||||
@@ -67,6 +70,7 @@ export function VMDisksList(p: {
|
|||||||
p.onChange?.();
|
p.onChange?.();
|
||||||
}}
|
}}
|
||||||
onRequestBackup={handleBackupRequest}
|
onRequestBackup={handleBackupRequest}
|
||||||
|
diskImagesList={p.diskImagesList}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -93,6 +97,7 @@ function DiskInfo(p: {
|
|||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
removeFromList: () => void;
|
removeFromList: () => void;
|
||||||
onRequestBackup: (disk: VMFileDisk) => void;
|
onRequestBackup: (disk: VMFileDisk) => void;
|
||||||
|
diskImagesList: DiskImage[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const deleteDisk = async () => {
|
const deleteDisk = async () => {
|
||||||
@@ -198,23 +203,6 @@ function DiskInfo(p: {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextInput
|
|
||||||
editable={true}
|
|
||||||
label="Disk size (GB)"
|
|
||||||
size={{
|
|
||||||
min:
|
|
||||||
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
|
|
||||||
max:
|
|
||||||
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
|
|
||||||
}}
|
|
||||||
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
|
|
||||||
onValueChange={(v) => {
|
|
||||||
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
|
|
||||||
p.onChange?.();
|
|
||||||
}}
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
editable={true}
|
editable={true}
|
||||||
label="Disk format"
|
label="Disk format"
|
||||||
@@ -243,6 +231,34 @@ function DiskInfo(p: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
editable={true}
|
||||||
|
label="Disk size (GB)"
|
||||||
|
size={{
|
||||||
|
min:
|
||||||
|
ServerApi.Config.constraints.disk_size.min / (1000 * 1000 * 1000),
|
||||||
|
max:
|
||||||
|
ServerApi.Config.constraints.disk_size.max / (1000 * 1000 * 1000),
|
||||||
|
}}
|
||||||
|
value={(p.disk.size / (1000 * 1000 * 1000)).toString()}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.disk.size = Number(v ?? "0") * 1000 * 1000 * 1000;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
disabled={!!p.disk.from_image}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DiskImageSelect
|
||||||
|
label="Use disk image as template"
|
||||||
|
list={p.diskImagesList}
|
||||||
|
value={p.disk.from_image}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.disk.from_image = v;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ import { VMDisksList } from "../forms/VMDisksList";
|
|||||||
import { VMNetworksList } from "../forms/VMNetworksList";
|
import { VMNetworksList } from "../forms/VMNetworksList";
|
||||||
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
|
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
|
||||||
import { VMScreenshot } from "./VMScreenshot";
|
import { VMScreenshot } from "./VMScreenshot";
|
||||||
|
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
|
||||||
|
|
||||||
interface DetailsProps {
|
interface DetailsProps {
|
||||||
vm: VMInfo;
|
vm: VMInfo;
|
||||||
@@ -38,6 +39,9 @@ interface DetailsProps {
|
|||||||
|
|
||||||
export function VMDetails(p: DetailsProps): React.ReactElement {
|
export function VMDetails(p: DetailsProps): React.ReactElement {
|
||||||
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
|
||||||
|
const [diskImagesList, setDiskImagesList] = React.useState<
|
||||||
|
DiskImage[] | undefined
|
||||||
|
>();
|
||||||
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
|
||||||
const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
|
const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
|
||||||
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
const [vcpuCombinations, setVCPUCombinations] = React.useState<
|
||||||
@@ -52,6 +56,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setGroupsList(await GroupApi.GetList());
|
setGroupsList(await GroupApi.GetList());
|
||||||
|
setDiskImagesList(await DiskImageApi.GetList());
|
||||||
setIsoList(await IsoFilesApi.GetList());
|
setIsoList(await IsoFilesApi.GetList());
|
||||||
setBridgesList(await ServerApi.GetNetworksBridgesList());
|
setBridgesList(await ServerApi.GetNetworksBridgesList());
|
||||||
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
setVCPUCombinations(await ServerApi.NumberVCPUs());
|
||||||
@@ -67,6 +72,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
|
|||||||
build={() => (
|
build={() => (
|
||||||
<VMDetailsInner
|
<VMDetailsInner
|
||||||
groupsList={groupsList!}
|
groupsList={groupsList!}
|
||||||
|
diskImagesList={diskImagesList!}
|
||||||
isoList={isoList!}
|
isoList={isoList!}
|
||||||
bridgesList={bridgesList!}
|
bridgesList={bridgesList!}
|
||||||
vcpuCombinations={vcpuCombinations!}
|
vcpuCombinations={vcpuCombinations!}
|
||||||
@@ -90,6 +96,7 @@ enum VMTab {
|
|||||||
|
|
||||||
type DetailsInnerProps = DetailsProps & {
|
type DetailsInnerProps = DetailsProps & {
|
||||||
groupsList: string[];
|
groupsList: string[];
|
||||||
|
diskImagesList: DiskImage[];
|
||||||
isoList: IsoFile[];
|
isoList: IsoFile[];
|
||||||
bridgesList: string[];
|
bridgesList: string[];
|
||||||
vcpuCombinations: number[];
|
vcpuCombinations: number[];
|
||||||
|
Reference in New Issue
Block a user