Created first disk
This commit is contained in:
		@@ -1,3 +1,4 @@
 | 
			
		||||
use crate::libvirt_lib_structures::DomainXMLUuid;
 | 
			
		||||
use clap::Parser;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
 | 
			
		||||
@@ -156,9 +157,18 @@ impl AppConfig {
 | 
			
		||||
        self.storage_path().join("iso")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get VM vnc sockets
 | 
			
		||||
    /// Get VM vnc sockets directory
 | 
			
		||||
    pub fn vnc_sockets_path(&self) -> PathBuf {
 | 
			
		||||
        self.storage_path().to_path_buf()
 | 
			
		||||
        self.storage_path().join("vnc")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get VM vnc sockets directory
 | 
			
		||||
    pub fn disks_storage_path(&self) -> PathBuf {
 | 
			
		||||
        self.storage_path().join("disks")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn vm_storage_path(&self, id: DomainXMLUuid) -> PathBuf {
 | 
			
		||||
        self.disks_storage_path().join(id.as_string())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,3 +31,15 @@ pub const MIN_VM_MEMORY: usize = 100;
 | 
			
		||||
 | 
			
		||||
/// Max VM memory size (MB)
 | 
			
		||||
pub const MAX_VM_MEMORY: usize = 64000;
 | 
			
		||||
 | 
			
		||||
/// Disk name min length
 | 
			
		||||
pub const DISK_NAME_MIN_LEN: usize = 2;
 | 
			
		||||
 | 
			
		||||
/// Disk name max length
 | 
			
		||||
pub const DISK_NAME_MAX_LEN: usize = 10;
 | 
			
		||||
 | 
			
		||||
/// Disk size min (MB)
 | 
			
		||||
pub const DISK_SIZE_MIN: usize = 100;
 | 
			
		||||
 | 
			
		||||
/// Disk size max (MB)
 | 
			
		||||
pub const DISK_SIZE_MAX: usize = 1000 * 1000 * 2;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::constants::{DISK_NAME_MAX_LEN, DISK_NAME_MIN_LEN, DISK_SIZE_MAX, DISK_SIZE_MIN};
 | 
			
		||||
use crate::controllers::{HttpResult, LibVirtReq};
 | 
			
		||||
use crate::extractors::local_auth_extractor::LocalAuthEnabled;
 | 
			
		||||
use crate::libvirt_rest_structures::HypervisorInfo;
 | 
			
		||||
@@ -31,6 +32,8 @@ struct ServerConstraints {
 | 
			
		||||
    name_size: LenConstraints,
 | 
			
		||||
    title_size: LenConstraints,
 | 
			
		||||
    memory_size: LenConstraints,
 | 
			
		||||
    disk_name_size: LenConstraints,
 | 
			
		||||
    disk_size: LenConstraints,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
			
		||||
@@ -48,6 +51,14 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
			
		||||
                min: constants::MIN_VM_MEMORY,
 | 
			
		||||
                max: constants::MAX_VM_MEMORY,
 | 
			
		||||
            },
 | 
			
		||||
            disk_name_size: LenConstraints {
 | 
			
		||||
                min: DISK_NAME_MIN_LEN,
 | 
			
		||||
                max: DISK_NAME_MAX_LEN,
 | 
			
		||||
            },
 | 
			
		||||
            disk_size: LenConstraints {
 | 
			
		||||
                min: DISK_SIZE_MIN,
 | 
			
		||||
                max: DISK_SIZE_MAX,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,9 @@ impl DomainXMLUuid {
 | 
			
		||||
        Ok(Self(uuid::Uuid::parse_str(s)?))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn new_random() -> Self {
 | 
			
		||||
        Self(uuid::Uuid::new_v4())
 | 
			
		||||
    }
 | 
			
		||||
    pub fn as_string(&self) -> String {
 | 
			
		||||
        self.0.to_string()
 | 
			
		||||
    }
 | 
			
		||||
@@ -93,7 +96,8 @@ pub struct DiskXML {
 | 
			
		||||
    pub driver: DiskDriverXML,
 | 
			
		||||
    pub source: DiskSourceXML,
 | 
			
		||||
    pub target: DiskTargetXML,
 | 
			
		||||
    pub readonly: DiskReadOnlyXML,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    pub readonly: Option<DiskReadOnlyXML>,
 | 
			
		||||
    pub boot: DiskBootXML,
 | 
			
		||||
    #[serde(skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    pub address: Option<DiskAddressXML>,
 | 
			
		||||
@@ -106,6 +110,8 @@ pub struct DiskDriverXML {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    #[serde(rename(serialize = "@type"))]
 | 
			
		||||
    pub r#type: String,
 | 
			
		||||
    #[serde(default, rename(serialize = "@cache"))]
 | 
			
		||||
    pub r#cache: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ use crate::libvirt_lib_structures::{
 | 
			
		||||
    ACPIXML, OSXML,
 | 
			
		||||
};
 | 
			
		||||
use crate::libvirt_rest_structures::LibVirtStructError::StructureExtraction;
 | 
			
		||||
use crate::utils::disks_utils::Disk;
 | 
			
		||||
use crate::utils::files_utils;
 | 
			
		||||
use lazy_regex::regex;
 | 
			
		||||
use std::ops::{Div, Mul};
 | 
			
		||||
@@ -74,7 +75,8 @@ pub struct VMInfo {
 | 
			
		||||
    pub vnc_access: bool,
 | 
			
		||||
    /// Attach an ISO file
 | 
			
		||||
    pub iso_file: Option<String>,
 | 
			
		||||
    // TODO : storage
 | 
			
		||||
    /// 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>,
 | 
			
		||||
    // TODO : autostart
 | 
			
		||||
    // TODO : network interface
 | 
			
		||||
}
 | 
			
		||||
@@ -86,11 +88,14 @@ impl VMInfo {
 | 
			
		||||
            return Err(StructureExtraction("VM name is invalid!").into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(n) = &self.uuid {
 | 
			
		||||
        let uuid = if let Some(n) = self.uuid {
 | 
			
		||||
            if !n.is_valid() {
 | 
			
		||||
                return Err(StructureExtraction("VM UUID is invalid!").into());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
            n
 | 
			
		||||
        } else {
 | 
			
		||||
            DomainXMLUuid::new_random()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if let Some(n) = &self.genid {
 | 
			
		||||
            if !n.is_valid() {
 | 
			
		||||
@@ -127,6 +132,7 @@ impl VMInfo {
 | 
			
		||||
                driver: DiskDriverXML {
 | 
			
		||||
                    name: "qemu".to_string(),
 | 
			
		||||
                    r#type: "raw".to_string(),
 | 
			
		||||
                    cache: "none".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                source: DiskSourceXML {
 | 
			
		||||
                    file: path.to_string_lossy().to_string(),
 | 
			
		||||
@@ -135,17 +141,11 @@ impl VMInfo {
 | 
			
		||||
                    dev: "hdc".to_string(),
 | 
			
		||||
                    bus: "usb".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                readonly: DiskReadOnlyXML {},
 | 
			
		||||
                readonly: Some(DiskReadOnlyXML {}),
 | 
			
		||||
                boot: DiskBootXML {
 | 
			
		||||
                    order: "1".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                address: None, /*DiskAddressXML {
 | 
			
		||||
                                   r#type: "drive".to_string(),
 | 
			
		||||
                                   controller: "0".to_string(),
 | 
			
		||||
                                   bus: "1".to_string(),
 | 
			
		||||
                                   target: "0".to_string(),
 | 
			
		||||
                                   unit: "0".to_string(),
 | 
			
		||||
                               },*/
 | 
			
		||||
                address: None,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -161,10 +161,52 @@ impl VMInfo {
 | 
			
		||||
            false => None,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Check disks name for duplicates
 | 
			
		||||
        for disk in &self.disks {
 | 
			
		||||
            if self.disks.iter().filter(|d| d.name == disk.name).count() > 1 {
 | 
			
		||||
                return Err(StructureExtraction("Two differents disks have the same name!").into());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Apply disks configuration
 | 
			
		||||
        for disk in self.disks {
 | 
			
		||||
            disk.check_config()?;
 | 
			
		||||
            disk.apply_config(uuid)?;
 | 
			
		||||
 | 
			
		||||
            if disk.delete {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            disks.push(DiskXML {
 | 
			
		||||
                r#type: "file".to_string(),
 | 
			
		||||
                device: "disk".to_string(),
 | 
			
		||||
                driver: DiskDriverXML {
 | 
			
		||||
                    name: "qemu".to_string(),
 | 
			
		||||
                    r#type: "raw".to_string(),
 | 
			
		||||
                    cache: "none".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                source: DiskSourceXML {
 | 
			
		||||
                    file: disk.disk_path(uuid).to_string_lossy().to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                target: DiskTargetXML {
 | 
			
		||||
                    dev: format!(
 | 
			
		||||
                        "vd{}",
 | 
			
		||||
                        ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"][disks.len()]
 | 
			
		||||
                    ),
 | 
			
		||||
                    bus: "virtio".to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                readonly: None,
 | 
			
		||||
                boot: DiskBootXML {
 | 
			
		||||
                    order: (disks.len() + 1).to_string(),
 | 
			
		||||
                },
 | 
			
		||||
                address: None,
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(DomainXML {
 | 
			
		||||
            r#type: "kvm".to_string(),
 | 
			
		||||
            name: self.name,
 | 
			
		||||
            uuid: self.uuid,
 | 
			
		||||
            uuid: Some(uuid),
 | 
			
		||||
            genid: self.genid.map(|i| i.0),
 | 
			
		||||
            title: self.title,
 | 
			
		||||
            description: self.description,
 | 
			
		||||
@@ -239,6 +281,14 @@ impl VMInfo {
 | 
			
		||||
                .iter()
 | 
			
		||||
                .find(|d| d.device == "cdrom")
 | 
			
		||||
                .map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string()),
 | 
			
		||||
 | 
			
		||||
            disks: domain
 | 
			
		||||
                .devices
 | 
			
		||||
                .disks
 | 
			
		||||
                .iter()
 | 
			
		||||
                .filter(|d| d.device == "disk")
 | 
			
		||||
                .map(|d| Disk::load_from_file(&d.source.file).unwrap())
 | 
			
		||||
                .collect(),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,9 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
 | 
			
		||||
 | 
			
		||||
    log::debug!("Create required directory, if missing");
 | 
			
		||||
    files_utils::create_directory_if_missing(&AppConfig::get().iso_storage_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().iso_storage_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
 | 
			
		||||
 | 
			
		||||
    let conn = Data::new(LibVirtClient(
 | 
			
		||||
        LibVirtActor::connect()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								virtweb_backend/src/utils/disks_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								virtweb_backend/src/utils/disks_utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
use crate::app_config::AppConfig;
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::libvirt_lib_structures::DomainXMLUuid;
 | 
			
		||||
use crate::utils::files_utils;
 | 
			
		||||
use lazy_regex::regex;
 | 
			
		||||
use std::os::linux::fs::MetadataExt;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
 | 
			
		||||
#[derive(thiserror::Error, Debug)]
 | 
			
		||||
enum DisksError {
 | 
			
		||||
    #[error("DiskParseError: {0}")]
 | 
			
		||||
    Parse(&'static str),
 | 
			
		||||
    #[error("DiskConfigError: {0}")]
 | 
			
		||||
    Config(&'static str),
 | 
			
		||||
    #[error("DiskCreateError")]
 | 
			
		||||
    Create,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Type of disk allocation
 | 
			
		||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub enum DiskAllocType {
 | 
			
		||||
    Fixed,
 | 
			
		||||
    Sparse,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize)]
 | 
			
		||||
pub struct Disk {
 | 
			
		||||
    /// Disk size, in megabytes
 | 
			
		||||
    pub size: usize,
 | 
			
		||||
    /// Disk name
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub alloc_type: DiskAllocType,
 | 
			
		||||
    /// Set this variable to true to delete the disk
 | 
			
		||||
    pub delete: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Disk {
 | 
			
		||||
    pub fn load_from_file(path: &str) -> anyhow::Result<Self> {
 | 
			
		||||
        let file = Path::new(path);
 | 
			
		||||
 | 
			
		||||
        if !file.is_file() {
 | 
			
		||||
            return Err(DisksError::Parse("Path is not a file!").into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let metadata = file.metadata()?;
 | 
			
		||||
 | 
			
		||||
        // Approximate estimation
 | 
			
		||||
        let is_sparse = metadata.len() / 512 >= metadata.st_blocks();
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            size: metadata.len() as usize / (1000 * 1000),
 | 
			
		||||
            name: path.rsplit_once('/').unwrap().1.to_string(),
 | 
			
		||||
            alloc_type: match is_sparse {
 | 
			
		||||
                true => DiskAllocType::Sparse,
 | 
			
		||||
                false => DiskAllocType::Fixed,
 | 
			
		||||
            },
 | 
			
		||||
            delete: false,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn check_config(&self) -> anyhow::Result<()> {
 | 
			
		||||
        if constants::DISK_NAME_MIN_LEN > self.name.len()
 | 
			
		||||
            || constants::DISK_NAME_MAX_LEN < self.name.len()
 | 
			
		||||
        {
 | 
			
		||||
            return Err(DisksError::Config("Disk name length is invalid").into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !regex!("^[a-zA-Z0-9]+$").is_match(&self.name) {
 | 
			
		||||
            return Err(DisksError::Config("Disk name contains invalid characters!").into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.size < constants::DISK_SIZE_MIN || self.size > constants::DISK_SIZE_MAX {
 | 
			
		||||
            return Err(DisksError::Config("Disk size is invalid!").into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get disk path
 | 
			
		||||
    pub fn disk_path(&self, id: DomainXMLUuid) -> PathBuf {
 | 
			
		||||
        let domain_dir = AppConfig::get().vm_storage_path(id);
 | 
			
		||||
        domain_dir.join(&self.name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Apply disk configuration
 | 
			
		||||
    pub fn apply_config(&self, id: DomainXMLUuid) -> anyhow::Result<()> {
 | 
			
		||||
        self.check_config()?;
 | 
			
		||||
 | 
			
		||||
        let file = self.disk_path(id);
 | 
			
		||||
        files_utils::create_directory_if_missing(file.parent().unwrap())?;
 | 
			
		||||
 | 
			
		||||
        // Delete file if requested
 | 
			
		||||
        if self.delete {
 | 
			
		||||
            if !file.exists() {
 | 
			
		||||
                log::debug!("File {file:?} does not exists, so it was not deleted");
 | 
			
		||||
                return Ok(());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            log::info!("Deleting {file:?}");
 | 
			
		||||
            std::fs::remove_file(file)?;
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if file.exists() {
 | 
			
		||||
            log::debug!("File {file:?} does not exists, so it was not touched");
 | 
			
		||||
            return Ok(());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut cmd = Command::new("/usr/bin/dd");
 | 
			
		||||
        cmd.arg("if=/dev/zero")
 | 
			
		||||
            .arg(format!("of={}", file.to_string_lossy()))
 | 
			
		||||
            .arg("bs=1M");
 | 
			
		||||
 | 
			
		||||
        match self.alloc_type {
 | 
			
		||||
            DiskAllocType::Fixed => cmd.arg(format!("count={}", self.size)),
 | 
			
		||||
            DiskAllocType::Sparse => cmd.arg(format!("seek={}", self.size)).arg("count=0"),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let res = cmd.output()?;
 | 
			
		||||
 | 
			
		||||
        if !res.status.success() {
 | 
			
		||||
            log::error!(
 | 
			
		||||
                "Failed to create disk! stderr={} stdout={}",
 | 
			
		||||
                String::from_utf8_lossy(&res.stderr),
 | 
			
		||||
                String::from_utf8_lossy(&res.stdout)
 | 
			
		||||
            );
 | 
			
		||||
            return Err(DisksError::Create.into());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
const INVALID_CHARS: [&str; 19] = [
 | 
			
		||||
    "@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
 | 
			
		||||
@@ -11,7 +11,8 @@ pub fn check_file_name(name: &str) -> bool {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create directory if missing
 | 
			
		||||
pub fn create_directory_if_missing(path: &PathBuf) -> anyhow::Result<()> {
 | 
			
		||||
pub fn create_directory_if_missing<P: AsRef<Path>>(path: P) -> anyhow::Result<()> {
 | 
			
		||||
    let path = path.as_ref();
 | 
			
		||||
    if !path.exists() {
 | 
			
		||||
        std::fs::create_dir_all(path)?;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
pub mod disks_utils;
 | 
			
		||||
pub mod files_utils;
 | 
			
		||||
pub mod rand_utils;
 | 
			
		||||
pub mod time_utils;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ export interface ServerConstraints {
 | 
			
		||||
  name_size: LenConstraint;
 | 
			
		||||
  title_size: LenConstraint;
 | 
			
		||||
  memory_size: LenConstraint;
 | 
			
		||||
  disk_name_size: LenConstraint;
 | 
			
		||||
  disk_size: LenConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LenConstraint {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,18 @@ export type VMState =
 | 
			
		||||
  | "PowerManagementSuspended"
 | 
			
		||||
  | "Other";
 | 
			
		||||
 | 
			
		||||
export type DiskAllocType = "Sparse" | "Fixed";
 | 
			
		||||
 | 
			
		||||
export interface VMDisk {
 | 
			
		||||
  size: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  alloc_type: DiskAllocType;
 | 
			
		||||
  delete: boolean;
 | 
			
		||||
 | 
			
		||||
  // application attribute
 | 
			
		||||
  new?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VMInfoInterface {
 | 
			
		||||
  name: string;
 | 
			
		||||
  uuid?: string;
 | 
			
		||||
@@ -28,6 +40,7 @@ interface VMInfoInterface {
 | 
			
		||||
  memory: number;
 | 
			
		||||
  vnc_access: boolean;
 | 
			
		||||
  iso_file?: string;
 | 
			
		||||
  disks: VMDisk[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class VMInfo implements VMInfoInterface {
 | 
			
		||||
@@ -41,6 +54,7 @@ export class VMInfo implements VMInfoInterface {
 | 
			
		||||
  memory: number;
 | 
			
		||||
  vnc_access: boolean;
 | 
			
		||||
  iso_file?: string;
 | 
			
		||||
  disks: VMDisk[];
 | 
			
		||||
 | 
			
		||||
  constructor(int: VMInfoInterface) {
 | 
			
		||||
    this.name = int.name;
 | 
			
		||||
@@ -53,6 +67,7 @@ export class VMInfo implements VMInfoInterface {
 | 
			
		||||
    this.memory = int.memory;
 | 
			
		||||
    this.vnc_access = int.vnc_access;
 | 
			
		||||
    this.iso_file = int.iso_file;
 | 
			
		||||
    this.disks = int.disks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static NewEmpty(): VMInfo {
 | 
			
		||||
@@ -62,6 +77,7 @@ export class VMInfo implements VMInfoInterface {
 | 
			
		||||
      architecture: "x86_64",
 | 
			
		||||
      memory: 1024,
 | 
			
		||||
      vnc_access: true,
 | 
			
		||||
      disks: [],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,19 @@ export function TextInput(p: {
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (((!p.editable && p.value) ?? "") === "") return <></>;
 | 
			
		||||
 | 
			
		||||
  let valueError = undefined;
 | 
			
		||||
  if (p.value && p.value.length > 0) {
 | 
			
		||||
    if (p.size?.min && p.type !== "number" && p.value.length < p.size.min)
 | 
			
		||||
      valueError = "Invalid value size";
 | 
			
		||||
    if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!";
 | 
			
		||||
    if (
 | 
			
		||||
      p.type === "number" &&
 | 
			
		||||
      p.size &&
 | 
			
		||||
      (Number(p.value) > p.size.max || Number(p.value) < p.size.min)
 | 
			
		||||
    )
 | 
			
		||||
      valueError = "Invalide size range!";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TextField
 | 
			
		||||
      label={p.label}
 | 
			
		||||
@@ -39,13 +52,8 @@ export function TextInput(p: {
 | 
			
		||||
      multiline={p.multiline}
 | 
			
		||||
      minRows={p.minRows}
 | 
			
		||||
      maxRows={p.maxRows}
 | 
			
		||||
      error={
 | 
			
		||||
        (p.checkValue &&
 | 
			
		||||
          p.value &&
 | 
			
		||||
          p.value.length > 0 &&
 | 
			
		||||
          !p.checkValue(p.value)) ||
 | 
			
		||||
        false
 | 
			
		||||
      }
 | 
			
		||||
      error={valueError !== undefined}
 | 
			
		||||
      helperText={valueError}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										114
									
								
								virtweb_frontend/src/widgets/forms/VMDisksList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								virtweb_frontend/src/widgets/forms/VMDisksList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Button,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemAvatar,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Paper,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { VMDisk, VMInfo } from "../../api/VMApi";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
import { mdiHarddisk } from "@mdi/js";
 | 
			
		||||
import { TextInput } from "./TextInput";
 | 
			
		||||
import { ServerApi } from "../../api/ServerApi";
 | 
			
		||||
import { SelectInput } from "./SelectInput";
 | 
			
		||||
 | 
			
		||||
export function VMDisksList(p: {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const addNewDisk = () => {
 | 
			
		||||
    p.vm.disks.push({
 | 
			
		||||
      alloc_type: "Sparse",
 | 
			
		||||
      size: 10000,
 | 
			
		||||
      delete: false,
 | 
			
		||||
      name: `disk${p.vm.disks.length}`,
 | 
			
		||||
      new: true,
 | 
			
		||||
    });
 | 
			
		||||
    p.onChange?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* disks list */}
 | 
			
		||||
      {p.vm.disks.map((d, num) => (
 | 
			
		||||
        <DiskInfo
 | 
			
		||||
          key={num}
 | 
			
		||||
          editable={p.editable}
 | 
			
		||||
          disk={d}
 | 
			
		||||
          onChange={p.onChange}
 | 
			
		||||
        />
 | 
			
		||||
      ))}
 | 
			
		||||
 | 
			
		||||
      {p.editable && <Button onClick={addNewDisk}>Add new disk</Button>}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function DiskInfo(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  disk: VMDisk;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (!p.editable || !p.disk.new)
 | 
			
		||||
    return (
 | 
			
		||||
      <ListItem>
 | 
			
		||||
        <ListItemAvatar>
 | 
			
		||||
          <Avatar>
 | 
			
		||||
            <Icon path={mdiHarddisk} />
 | 
			
		||||
          </Avatar>
 | 
			
		||||
        </ListItemAvatar>
 | 
			
		||||
        <ListItemText
 | 
			
		||||
          primary={p.disk.name}
 | 
			
		||||
          secondary={`${filesize(p.disk.size * 1000 * 1000)} - ${
 | 
			
		||||
            p.disk.alloc_type
 | 
			
		||||
          }`}
 | 
			
		||||
        />
 | 
			
		||||
        {/* TODO delete disk if editable */}
 | 
			
		||||
      </ListItem>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Paper elevation={3} style={{ margin: "10px", padding: "10px" }}>
 | 
			
		||||
      <TextInput
 | 
			
		||||
        editable={true}
 | 
			
		||||
        label="Disk name"
 | 
			
		||||
        size={ServerApi.Config.constraints.disk_name_size}
 | 
			
		||||
        checkValue={(v) => /^[a-zA-Z0-9]+$/.test(v)}
 | 
			
		||||
        value={p.disk.name}
 | 
			
		||||
        onValueChange={(v) => {
 | 
			
		||||
          p.disk.name = v ?? "";
 | 
			
		||||
          p.onChange?.();
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <TextInput
 | 
			
		||||
        editable={true}
 | 
			
		||||
        label="Disk size (MB)"
 | 
			
		||||
        size={ServerApi.Config.constraints.disk_size}
 | 
			
		||||
        value={p.disk.size.toString()}
 | 
			
		||||
        onValueChange={(v) => {
 | 
			
		||||
          p.disk.size = Number(v ?? "0");
 | 
			
		||||
          p.onChange?.();
 | 
			
		||||
        }}
 | 
			
		||||
        type="number"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <SelectInput
 | 
			
		||||
        editable={true}
 | 
			
		||||
        label="File allocation type"
 | 
			
		||||
        options={[
 | 
			
		||||
          { label: "Sparse allocation", value: "Sparse" },
 | 
			
		||||
          { label: "Fixed allocation", value: "Fixed" },
 | 
			
		||||
        ]}
 | 
			
		||||
        value={p.disk.alloc_type}
 | 
			
		||||
        onValueChange={(v) => {
 | 
			
		||||
          p.disk.alloc_type = v as any;
 | 
			
		||||
          p.onChange?.();
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Paper>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,7 @@ import { IsoFile, IsoFilesApi } from "../../api/IsoFilesApi";
 | 
			
		||||
import { AsyncWidget } from "../AsyncWidget";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { filesize } from "filesize";
 | 
			
		||||
import { VMDisksList } from "../forms/VMDisksList";
 | 
			
		||||
 | 
			
		||||
interface DetailsProps {
 | 
			
		||||
  vm: VMInfo;
 | 
			
		||||
@@ -174,6 +175,7 @@ function VMDetailsInner(
 | 
			
		||||
            }),
 | 
			
		||||
          ]}
 | 
			
		||||
        />
 | 
			
		||||
        <VMDisksList vm={p.vm} editable={p.editable} onChange={p.onChange} />
 | 
			
		||||
      </EditSection>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user