2 Commits

Author SHA1 Message Date
a18310e04a Simplify RAM management
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-30 09:20:49 +02:00
dd7f9176fa Clarify some constants 2025-05-30 09:03:00 +02:00
10 changed files with 85 additions and 73 deletions

View File

@ -27,20 +27,20 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
];
/// ISO max size
pub const ISO_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000;
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
/// Allowed uploaded disk images formats
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 2] =
["application/x-qemu-disk", "application/gzip"];
/// Disk image max size
pub const DISK_IMAGE_MAX_SIZE: usize = 10 * 1000 * 1000 * 1000 * 1000;
pub const DISK_IMAGE_MAX_SIZE: FileSize = FileSize::from_gb(10 * 1000);
/// Min VM memory size (MB)
pub const MIN_VM_MEMORY: usize = 100;
/// Min VM memory size
pub const MIN_VM_MEMORY: FileSize = FileSize::from_mb(100);
/// Max VM memory size (MB)
pub const MAX_VM_MEMORY: usize = 64000;
/// Max VM memory size
pub const MAX_VM_MEMORY: FileSize = FileSize::from_gb(64);
/// Disk name min length
pub const DISK_NAME_MIN_LEN: usize = 2;

View File

@ -24,7 +24,7 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
let file = form.files.remove(0);
// Check uploaded file size
if file.size > constants::DISK_IMAGE_MAX_SIZE {
if file.size > constants::DISK_IMAGE_MAX_SIZE.as_bytes() {
return Ok(HttpResponse::BadRequest().json("Disk image max size exceeded!"));
}

View File

@ -26,7 +26,7 @@ pub async fn upload_file(MultipartForm(mut form): MultipartForm<UploadIsoForm>)
let file = form.files.remove(0);
if file.size > constants::ISO_MAX_SIZE {
if file.size > constants::ISO_MAX_SIZE.as_bytes() {
log::error!("Uploaded ISO file is too large!");
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
@ -88,7 +88,7 @@ pub async fn upload_from_url(req: web::Json<DownloadFromURLReq>) -> HttpResult {
let response = reqwest::get(&req.url).await?;
if let Some(len) = response.content_length() {
if len > constants::ISO_MAX_SIZE as u64 {
if len > constants::ISO_MAX_SIZE.as_bytes() as u64 {
return Ok(HttpResponse::BadRequest().json("File is too large!"));
}
}

View File

@ -71,8 +71,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
builtin_nwfilter_rules: &constants::BUILTIN_NETWORK_FILTER_RULES,
nwfilter_chains: &constants::NETWORK_CHAINS,
constraints: ServerConstraints {
iso_max_size: constants::ISO_MAX_SIZE,
disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE,
iso_max_size: constants::ISO_MAX_SIZE.as_bytes(),
disk_image_max_size: constants::DISK_IMAGE_MAX_SIZE.as_bytes(),
vnc_token_duration: VNC_TOKEN_LIFETIME,
@ -80,8 +80,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
vm_title_size: LenConstraints { min: 0, max: 50 },
group_id_size: LenConstraints { min: 3, max: 50 },
memory_size: LenConstraints {
min: constants::MIN_VM_MEMORY,
max: constants::MAX_VM_MEMORY,
min: constants::MIN_VM_MEMORY.as_bytes(),
max: constants::MAX_VM_MEMORY.as_bytes(),
},
disk_name_size: LenConstraints {
min: DISK_NAME_MIN_LEN,

View File

@ -4,8 +4,8 @@ use crate::libvirt_lib_structures::XMLUuid;
use crate::libvirt_lib_structures::domain::*;
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::files_utils::convert_size_unit_to_mb;
use crate::utils::vm_file_disks_utils::{VMDiskFormat, VMFileDisk};
use lazy_regex::regex;
use num::Integer;
@ -70,8 +70,8 @@ pub struct VMInfo {
pub group: Option<VMGroupId>,
pub boot_type: BootType,
pub architecture: VMArchitecture,
/// VM allocated memory, in megabytes
pub memory: usize,
/// VM allocated RAM memory
pub memory: FileSize,
/// Number of vCPU for the VM
pub number_vcpu: usize,
/// Enable VNC access through admin console
@ -380,7 +380,7 @@ impl VMInfo {
memory: DomainMemoryXML {
unit: "MB".to_string(),
memory: self.memory,
memory: self.memory.as_mb(),
},
vcpu: DomainVCPUXML {
@ -452,7 +452,7 @@ impl VMInfo {
}
},
number_vcpu: domain.vcpu.body,
memory: convert_size_unit_to_mb(&domain.memory.unit, domain.memory.memory)?,
memory: FileSize::from_size_unit(&domain.memory.unit, domain.memory.memory)?,
vnc_access: domain.devices.graphics.is_some(),
iso_files: domain
.devices

View File

@ -122,10 +122,9 @@ async fn main() -> std::io::Result<()> {
}))
.app_data(conn.clone())
// Uploaded files
.app_data(
MultipartFormConfig::default()
.total_limit(max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE)),
)
.app_data(MultipartFormConfig::default().total_limit(
max(constants::DISK_IMAGE_MAX_SIZE, constants::ISO_MAX_SIZE).as_bytes(),
))
.app_data(TempFileConfig::default().directory(&AppConfig::get().temp_dir))
// Server controller
.route(

View File

@ -1,3 +1,12 @@
use std::ops::Mul;
#[derive(thiserror::Error, Debug)]
enum FilesSizeUtilsError {
#[error("UnitConvertError: {0}")]
UnitConvert(String),
}
/// Holds a data size, convertible in any form
#[derive(
serde::Serialize,
serde::Deserialize,
@ -25,6 +34,30 @@ impl FileSize {
Self(gb * 1000 * 1000 * 1000)
}
/// Convert size unit to MB
pub fn from_size_unit(unit: &str, value: usize) -> anyhow::Result<Self> {
let fact = match unit {
"bytes" | "b" => 1f64,
"KB" => 1000f64,
"MB" => 1000f64 * 1000f64,
"GB" => 1000f64 * 1000f64 * 1000f64,
"TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64,
"k" | "KiB" => 1024f64,
"M" | "MiB" => 1024f64 * 1024f64,
"G" | "GiB" => 1024f64 * 1024f64 * 1024f64,
"T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64,
_ => {
return Err(
FilesSizeUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into(),
);
}
};
Ok(Self((value as f64).mul(fact) as usize))
}
/// Get file size as bytes
pub fn as_bytes(&self) -> usize {
self.0
@ -35,3 +68,24 @@ impl FileSize {
self.0 / (1000 * 1000)
}
}
#[cfg(test)]
mod tests {
use crate::utils::file_size_utils::FileSize;
#[test]
fn convert_units_mb() {
assert_eq!(FileSize::from_size_unit("MB", 1).unwrap().as_mb(), 1);
assert_eq!(FileSize::from_size_unit("MB", 1000).unwrap().as_mb(), 1000);
assert_eq!(
FileSize::from_size_unit("GB", 1000).unwrap().as_mb(),
1000 * 1000
);
assert_eq!(FileSize::from_size_unit("GB", 1).unwrap().as_mb(), 1000);
assert_eq!(FileSize::from_size_unit("GiB", 3).unwrap().as_mb(), 3221);
assert_eq!(
FileSize::from_size_unit("KiB", 488281).unwrap().as_mb(),
499
);
}
}

View File

@ -1,13 +1,6 @@
use std::ops::{Div, Mul};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
#[derive(thiserror::Error, Debug)]
enum FilesUtilsError {
#[error("UnitConvertError: {0}")]
UnitConvert(String),
}
const INVALID_CHARS: [&str; 19] = [
"@", "\\", "/", ":", ",", "<", ">", "%", "'", "\"", "?", "{", "}", "$", "*", "|", ";", "=",
"\t",
@ -35,31 +28,9 @@ pub fn set_file_permission<P: AsRef<Path>>(path: P, mode: u32) -> anyhow::Result
Ok(())
}
/// Convert size unit to MB
pub fn convert_size_unit_to_mb(unit: &str, value: usize) -> anyhow::Result<usize> {
let fact = match unit {
"bytes" | "b" => 1f64,
"KB" => 1000f64,
"MB" => 1000f64 * 1000f64,
"GB" => 1000f64 * 1000f64 * 1000f64,
"TB" => 1000f64 * 1000f64 * 1000f64 * 1000f64,
"k" | "KiB" => 1024f64,
"M" | "MiB" => 1024f64 * 1024f64,
"G" | "GiB" => 1024f64 * 1024f64 * 1024f64,
"T" | "TiB" => 1024f64 * 1024f64 * 1024f64 * 1024f64,
_ => {
return Err(FilesUtilsError::UnitConvert(format!("Unknown size unit: {unit}")).into());
}
};
Ok((value as f64).mul(fact.div((1000 * 1000) as f64)).ceil() as usize)
}
#[cfg(test)]
mod test {
use crate::utils::files_utils::{check_file_name, convert_size_unit_to_mb};
use crate::utils::files_utils::check_file_name;
#[test]
fn empty_file_name() {
@ -85,14 +56,4 @@ mod test {
fn valid_file_name() {
assert!(check_file_name("test.iso"));
}
#[test]
fn convert_units_mb() {
assert_eq!(convert_size_unit_to_mb("MB", 1).unwrap(), 1);
assert_eq!(convert_size_unit_to_mb("MB", 1000).unwrap(), 1000);
assert_eq!(convert_size_unit_to_mb("GB", 1000).unwrap(), 1000 * 1000);
assert_eq!(convert_size_unit_to_mb("GB", 1).unwrap(), 1000);
assert_eq!(convert_size_unit_to_mb("GiB", 3).unwrap(), 3222);
assert_eq!(convert_size_unit_to_mb("KiB", 488281).unwrap(), 500);
}
}

View File

@ -154,7 +154,7 @@ function VMListWidget(p: {
{row.name}
</TableCell>
<TableCell>{row.description ?? ""}</TableCell>
<TableCell>{vmMemoryToHuman(row.memory)}</TableCell>
<TableCell>{filesize(row.memory)}</TableCell>
<TableCell>{row.number_vcpu}</TableCell>
<TableCell>
<VMStatusWidget
@ -183,13 +183,13 @@ function VMListWidget(p: {
<TableCell></TableCell>
<TableCell></TableCell>
<TableCell>
{vmMemoryToHuman(
{filesize(
p.list
.filter((v) => runningVMs.has(v.name))
.reduce((s, v) => s + v.memory, 0)
)}
{" / "}
{vmMemoryToHuman(p.list.reduce((s, v) => s + v.memory, 0))}
{filesize(p.list.reduce((s, v) => s + v.memory, 0))}
</TableCell>
<TableCell>
{p.list
@ -206,7 +206,3 @@ function VMListWidget(p: {
</TableContainer>
);
}
function vmMemoryToHuman(size: number): string {
return filesize(size * 1000 * 1000);
}

View File

@ -279,14 +279,16 @@ function VMDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
label="Memory (MB)"
editable={p.editable}
type="number"
value={p.vm.memory.toString()}
value={Math.floor(p.vm.memory / (1000 * 1000)).toString()}
onValueChange={(v) => {
p.vm.memory = Number(v ?? "0");
p.vm.memory = Number(v ?? "0") * 1000 * 1000;
p.onChange?.();
}}
checkValue={(v) =>
Number(v) > ServerApi.Config.constraints.memory_size.min &&
Number(v) < ServerApi.Config.constraints.memory_size.max
Number(v) >
ServerApi.Config.constraints.memory_size.min / (1000 * 1000) &&
Number(v) <
ServerApi.Config.constraints.memory_size.max / (1000 * 1000)
}
/>