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")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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
 | 
			
		||||
    pub fn vnc_sockets_path(&self) -> PathBuf {
 | 
			
		||||
        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
 | 
			
		||||
    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() {
 | 
			
		||||
        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!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let file_path = AppConfig::get()
 | 
			
		||||
        .disk_images_storage_path()
 | 
			
		||||
        .join(&p.filename);
 | 
			
		||||
    let file_path = AppConfig::get().disk_images_file_path(&p.filename);
 | 
			
		||||
 | 
			
		||||
    if !file_path.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!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let src_file_path = AppConfig::get()
 | 
			
		||||
        .disk_images_storage_path()
 | 
			
		||||
        .join(&p.filename);
 | 
			
		||||
    let src_file_path = AppConfig::get().disk_images_file_path(&p.filename);
 | 
			
		||||
 | 
			
		||||
    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!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let dst_file_path = AppConfig::get()
 | 
			
		||||
        .disk_images_storage_path()
 | 
			
		||||
        .join(&req.dest_file_name);
 | 
			
		||||
    let dst_file_path = AppConfig::get().disk_images_file_path(&req.dest_file_name);
 | 
			
		||||
 | 
			
		||||
    if dst_file_path.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!"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let file_path = AppConfig::get()
 | 
			
		||||
        .disk_images_storage_path()
 | 
			
		||||
        .join(&p.filename);
 | 
			
		||||
    let file_path = AppConfig::get().disk_images_file_path(&p.filename);
 | 
			
		||||
 | 
			
		||||
    if !file_path.exists() {
 | 
			
		||||
        return Ok(HttpResponse::NotFound().json("Disk image does not exists!"));
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,9 @@ pub struct VMFileDisk {
 | 
			
		||||
    /// Disk format
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    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
 | 
			
		||||
    pub delete: bool,
 | 
			
		||||
}
 | 
			
		||||
@@ -59,6 +62,7 @@ impl VMFileDisk {
 | 
			
		||||
                _ => anyhow::bail!("Unsupported image format: {:?}", info.format),
 | 
			
		||||
            },
 | 
			
		||||
            delete: false,
 | 
			
		||||
            from_image: None,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -78,6 +82,19 @@ impl VMFileDisk {
 | 
			
		||||
            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(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -115,17 +132,27 @@ impl VMFileDisk {
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create disk file
 | 
			
		||||
        DiskFileInfo::create(
 | 
			
		||||
            &file,
 | 
			
		||||
            match self.format {
 | 
			
		||||
                VMDiskFormat::Raw { is_sparse } => DiskFileFormat::Raw { is_sparse },
 | 
			
		||||
                VMDiskFormat::QCow2 => DiskFileFormat::QCow2 {
 | 
			
		||||
                    virtual_size: self.size,
 | 
			
		||||
                },
 | 
			
		||||
        let format = match self.format {
 | 
			
		||||
            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(())
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ export interface BaseFileVMDisk {
 | 
			
		||||
  name: string;
 | 
			
		||||
  delete: boolean;
 | 
			
		||||
 | 
			
		||||
  // For new disk only
 | 
			
		||||
  from_image?: string;
 | 
			
		||||
 | 
			
		||||
  // application attributes
 | 
			
		||||
  new?: boolean;
 | 
			
		||||
  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;
 | 
			
		||||
  style?: React.CSSProperties;
 | 
			
		||||
  helperText?: string;
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (!p.editable && (p.value ?? "") === "") return <></>;
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +36,7 @@ export function TextInput(p: {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TextField
 | 
			
		||||
      disabled={p.disabled}
 | 
			
		||||
      label={p.label}
 | 
			
		||||
      value={p.value ?? ""}
 | 
			
		||||
      onChange={(e) =>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,12 +21,15 @@ import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
import { CheckboxInput } from "./CheckboxInput";
 | 
			
		||||
import { SelectInput } from "./SelectInput";
 | 
			
		||||
import { TextInput } from "./TextInput";
 | 
			
		||||
import { DiskImageSelect } from "./DiskImageSelect";
 | 
			
		||||
import { DiskImage } from "../../api/DiskImageApi";
 | 
			
		||||
 | 
			
		||||
export function VMDisksList(p: {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
  state?: VMState;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  diskImagesList: DiskImage[];
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const [currBackupRequest, setCurrBackupRequest] = React.useState<
 | 
			
		||||
    VMFileDisk | undefined
 | 
			
		||||
@@ -67,6 +70,7 @@ export function VMDisksList(p: {
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
          onRequestBackup={handleBackupRequest}
 | 
			
		||||
          diskImagesList={p.diskImagesList}
 | 
			
		||||
        />
 | 
			
		||||
      ))}
 | 
			
		||||
 | 
			
		||||
@@ -93,6 +97,7 @@ function DiskInfo(p: {
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  removeFromList: () => void;
 | 
			
		||||
  onRequestBackup: (disk: VMFileDisk) => void;
 | 
			
		||||
  diskImagesList: DiskImage[];
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const deleteDisk = async () => {
 | 
			
		||||
@@ -198,23 +203,6 @@ function DiskInfo(p: {
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </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
 | 
			
		||||
        editable={true}
 | 
			
		||||
        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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ import { VMDisksList } from "../forms/VMDisksList";
 | 
			
		||||
import { VMNetworksList } from "../forms/VMNetworksList";
 | 
			
		||||
import { VMSelectIsoInput } from "../forms/VMSelectIsoInput";
 | 
			
		||||
import { VMScreenshot } from "./VMScreenshot";
 | 
			
		||||
import { DiskImage, DiskImageApi } from "../../api/DiskImageApi";
 | 
			
		||||
 | 
			
		||||
interface DetailsProps {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
@@ -38,6 +39,9 @@ interface DetailsProps {
 | 
			
		||||
 | 
			
		||||
export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
			
		||||
  const [groupsList, setGroupsList] = React.useState<string[] | undefined>();
 | 
			
		||||
  const [diskImagesList, setDiskImagesList] = React.useState<
 | 
			
		||||
    DiskImage[] | undefined
 | 
			
		||||
  >();
 | 
			
		||||
  const [isoList, setIsoList] = React.useState<IsoFile[] | undefined>();
 | 
			
		||||
  const [bridgesList, setBridgesList] = React.useState<string[] | undefined>();
 | 
			
		||||
  const [vcpuCombinations, setVCPUCombinations] = React.useState<
 | 
			
		||||
@@ -52,6 +56,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setGroupsList(await GroupApi.GetList());
 | 
			
		||||
    setDiskImagesList(await DiskImageApi.GetList());
 | 
			
		||||
    setIsoList(await IsoFilesApi.GetList());
 | 
			
		||||
    setBridgesList(await ServerApi.GetNetworksBridgesList());
 | 
			
		||||
    setVCPUCombinations(await ServerApi.NumberVCPUs());
 | 
			
		||||
@@ -67,6 +72,7 @@ export function VMDetails(p: DetailsProps): React.ReactElement {
 | 
			
		||||
      build={() => (
 | 
			
		||||
        <VMDetailsInner
 | 
			
		||||
          groupsList={groupsList!}
 | 
			
		||||
          diskImagesList={diskImagesList!}
 | 
			
		||||
          isoList={isoList!}
 | 
			
		||||
          bridgesList={bridgesList!}
 | 
			
		||||
          vcpuCombinations={vcpuCombinations!}
 | 
			
		||||
@@ -90,6 +96,7 @@ enum VMTab {
 | 
			
		||||
 | 
			
		||||
type DetailsInnerProps = DetailsProps & {
 | 
			
		||||
  groupsList: string[];
 | 
			
		||||
  diskImagesList: DiskImage[];
 | 
			
		||||
  isoList: IsoFile[];
 | 
			
		||||
  bridgesList: string[];
 | 
			
		||||
  vcpuCombinations: number[];
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user