Created first disk

This commit is contained in:
Pierre HUBERT 2023-10-26 11:43:05 +02:00
parent 081b0f7784
commit bdb2f6427d
14 changed files with 393 additions and 25 deletions

View File

@ -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())
}
}

View File

@ -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;

View File

@ -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,
},
},
})
}

View File

@ -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)]

View File

@ -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(),
})
}
}

View File

@ -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()

View 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(())
}
}

View File

@ -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)?;
}

View File

@ -1,3 +1,4 @@
pub mod disks_utils;
pub mod files_utils;
pub mod rand_utils;
pub mod time_utils;

View File

@ -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 {

View File

@ -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: [],
});
}

View File

@ -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}
/>
);
}

View 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>
);
}

View File

@ -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>
);