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;
|
||||
|
Reference in New Issue
Block a user