Add QCow2 file format support on backend
This commit is contained in:
parent
01f26c1a79
commit
1e8394b3c4
@ -4,7 +4,7 @@ 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::disks_utils::Disk;
|
||||
use crate::utils::file_disks_utils::{DiskFormat, FileDisk};
|
||||
use crate::utils::files_utils;
|
||||
use crate::utils::files_utils::convert_size_unit_to_mb;
|
||||
use lazy_regex::regex;
|
||||
@ -78,7 +78,7 @@ pub struct VMInfo {
|
||||
/// Attach ISO file(s)
|
||||
pub iso_files: Vec<String>,
|
||||
/// 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>,
|
||||
pub disks: Vec<FileDisk>,
|
||||
/// Network cards
|
||||
pub networks: Vec<Network>,
|
||||
/// Add a TPM v2.0 module
|
||||
@ -129,6 +129,7 @@ impl VMInfo {
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
// Add ISO files
|
||||
for iso_file in &self.iso_files {
|
||||
if !files_utils::check_file_name(iso_file) {
|
||||
return Err(StructureExtraction("ISO filename is invalid!").into());
|
||||
@ -267,7 +268,10 @@ impl VMInfo {
|
||||
device: "disk".to_string(),
|
||||
driver: DiskDriverXML {
|
||||
name: "qemu".to_string(),
|
||||
r#type: "raw".to_string(),
|
||||
r#type: match disk.format {
|
||||
DiskFormat::Raw { .. } => "raw".to_string(),
|
||||
DiskFormat::QCow2 => "qcow2".to_string(),
|
||||
},
|
||||
cache: "none".to_string(),
|
||||
},
|
||||
source: DiskSourceXML {
|
||||
@ -429,7 +433,7 @@ impl VMInfo {
|
||||
.disks
|
||||
.iter()
|
||||
.filter(|d| d.device == "disk")
|
||||
.map(|d| Disk::load_from_file(&d.source.file).unwrap())
|
||||
.map(|d| FileDisk:: load_from_file(&d.source.file).unwrap())
|
||||
.collect(),
|
||||
|
||||
networks: domain
|
||||
|
@ -1,133 +0,0 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use crate::libvirt_lib_structures::XMLUuid;
|
||||
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: XMLUuid) -> PathBuf {
|
||||
let domain_dir = AppConfig::get().vm_storage_path(id);
|
||||
domain_dir.join(&self.name)
|
||||
}
|
||||
|
||||
/// Apply disk configuration
|
||||
pub fn apply_config(&self, id: XMLUuid) -> 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(())
|
||||
}
|
||||
}
|
208
virtweb_backend/src/utils/file_disks_utils.rs
Normal file
208
virtweb_backend/src/utils/file_disks_utils.rs
Normal file
@ -0,0 +1,208 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use crate::libvirt_lib_structures::XMLUuid;
|
||||
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,
|
||||
}
|
||||
|
||||
/// Disk allocation type
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "format")]
|
||||
pub enum DiskFormat {
|
||||
Raw {
|
||||
/// Type of disk allocation
|
||||
alloc_type: DiskAllocType,
|
||||
},
|
||||
QCow2,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct FileDisk {
|
||||
/// Disk name
|
||||
pub name: String,
|
||||
/// Disk size, in megabytes
|
||||
size: usize,
|
||||
/// Disk format
|
||||
#[serde(flatten)]
|
||||
pub format: DiskFormat,
|
||||
/// Set this variable to true to delete the disk
|
||||
pub delete: bool,
|
||||
}
|
||||
|
||||
impl FileDisk {
|
||||
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()?;
|
||||
let name = file.file_stem().and_then(|s| s.to_str()).unwrap_or("disk");
|
||||
let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("raw");
|
||||
|
||||
// Approximate raw file estimation
|
||||
let is_raw_sparse = metadata.len() / 512 >= metadata.st_blocks();
|
||||
|
||||
let format = match ext {
|
||||
"qcow2" => DiskFormat::QCow2,
|
||||
"raw" => DiskFormat::Raw {
|
||||
alloc_type: match is_raw_sparse {
|
||||
true => DiskAllocType::Sparse,
|
||||
false => DiskAllocType::Fixed,
|
||||
},
|
||||
},
|
||||
_ => anyhow::bail!("Unsupported disk extension: {ext}!"),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
size: match format {
|
||||
DiskFormat::Raw { .. } => metadata.len() as usize / (1000 * 1000),
|
||||
DiskFormat::QCow2 => qcow_virt_size(path)? / (1000 * 1000),
|
||||
},
|
||||
format,
|
||||
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());
|
||||
}
|
||||
|
||||
// Check disk size
|
||||
|
||||
if !(constants::DISK_SIZE_MIN..=constants::DISK_SIZE_MAX).contains(&self.size) {
|
||||
return Err(DisksError::Config("Disk size is invalid!").into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get disk path
|
||||
pub fn disk_path(&self, id: XMLUuid) -> PathBuf {
|
||||
let domain_dir = AppConfig::get().vm_storage_path(id);
|
||||
let file_name = match self.format {
|
||||
DiskFormat::Raw { .. } => self.name.to_string(),
|
||||
DiskFormat::QCow2 => format!("{}.qcow2", self.name),
|
||||
};
|
||||
domain_dir.join(&file_name)
|
||||
}
|
||||
|
||||
/// Apply disk configuration
|
||||
pub fn apply_config(&self, id: XMLUuid) -> 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(());
|
||||
}
|
||||
|
||||
// Prepare command to create file
|
||||
let res = match self.format {
|
||||
DiskFormat::Raw { alloc_type } => {
|
||||
let mut cmd = Command::new("/usr/bin/dd");
|
||||
cmd.arg("if=/dev/zero")
|
||||
.arg(format!("of={}", file.to_string_lossy()))
|
||||
.arg("bs=1M");
|
||||
|
||||
match alloc_type {
|
||||
DiskAllocType::Fixed => cmd.arg(format!("count={}", self.size)),
|
||||
DiskAllocType::Sparse => cmd.arg(format!("seek={}", self.size)).arg("count=0"),
|
||||
};
|
||||
|
||||
cmd.output()?
|
||||
}
|
||||
|
||||
DiskFormat::QCow2 => {
|
||||
let mut cmd = Command::new("/usr/bin/qemu-img");
|
||||
cmd.arg("create")
|
||||
.arg("-f")
|
||||
.arg("qcow2")
|
||||
.arg(file)
|
||||
.arg(format!("{}M", self.size));
|
||||
|
||||
cmd.output()?
|
||||
}
|
||||
};
|
||||
|
||||
// Execute Linux command
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct QCowInfoOutput {
|
||||
#[serde(rename = "virtual-size")]
|
||||
virtual_size: usize,
|
||||
}
|
||||
|
||||
/// Get QCow2 virtual size
|
||||
fn qcow_virt_size(path: &str) -> anyhow::Result<usize> {
|
||||
// Run qemu-img
|
||||
let mut cmd = Command::new("qemu-img");
|
||||
cmd.args(["info", path, "--output", "json", "--force-share"]);
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"qemu-img info failed, status: {}, stderr: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
let res_json = String::from_utf8(output.stdout)?;
|
||||
|
||||
// Decode JSON
|
||||
let decoded: QCowInfoOutput = serde_json::from_str(&res_json)?;
|
||||
Ok(decoded.virtual_size)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
pub mod disks_utils;
|
||||
pub mod file_disks_utils;
|
||||
pub mod files_utils;
|
||||
pub mod net_utils;
|
||||
pub mod rand_utils;
|
||||
|
11
virtweb_docs/REFERENCE.md
Normal file
11
virtweb_docs/REFERENCE.md
Normal file
@ -0,0 +1,11 @@
|
||||
## References
|
||||
|
||||
### LibVirt XML documentation
|
||||
* Online: https://libvirt.org/format.html
|
||||
|
||||
* Offline with Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt install libvirt-doc
|
||||
firefox /usr/share/doc/libvirt-doc/html/index.html
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user