Compare commits
5 Commits
33d70c2ee0
...
3680ceed61
Author | SHA1 | Date | |
---|---|---|---|
3680ceed61 | |||
8a7712ec42 | |||
9609cfb33a | |||
1fe7c60f36 | |||
f1339f0711 |
51
virtweb_backend/Cargo.lock
generated
51
virtweb_backend/Cargo.lock
generated
@ -1622,18 +1622,23 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"ipnet",
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1839,6 +1844,16 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@ -2773,9 +2788,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.15"
|
||||
version = "0.12.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
@ -2798,23 +2813,22 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2923,15 +2937,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.12.0"
|
||||
@ -3522,6 +3527,24 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"http-body",
|
||||
"iri-string",
|
||||
"pin-project-lite",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
|
@ -27,7 +27,7 @@ futures-util = "0.3.31"
|
||||
anyhow = "1.0.98"
|
||||
actix-multipart = "0.7.2"
|
||||
tempfile = "3.20.0"
|
||||
reqwest = { version = "0.12.15", features = ["stream"] }
|
||||
reqwest = { version = "0.12.19", features = ["stream"] }
|
||||
url = "2.5.4"
|
||||
virt = "0.4.2"
|
||||
sysinfo = { version = "0.35.1", features = ["serde"] }
|
||||
|
@ -182,6 +182,13 @@ impl Handler<DeleteDomainReq> for LibVirtActor {
|
||||
false => sys::VIR_DOMAIN_UNDEFINE_NVRAM,
|
||||
})?;
|
||||
|
||||
// Delete associated cloud init disk
|
||||
let cloud_init_disk = AppConfig::get().cloud_init_disk_path_for_vm(&domain_name);
|
||||
if cloud_init_disk.exists() {
|
||||
std::fs::remove_file(cloud_init_disk)?;
|
||||
}
|
||||
|
||||
// If requested, delete block storage associated with the VM
|
||||
if !msg.keep_files {
|
||||
log::info!("Delete storage associated with the domain");
|
||||
let path = AppConfig::get().vm_storage_path(msg.id);
|
||||
|
@ -250,6 +250,19 @@ impl AppConfig {
|
||||
self.storage_path().join("iso")
|
||||
}
|
||||
|
||||
/// Get the path where generated cloud init disk image are stored
|
||||
pub fn cloud_init_disk_storage_path(&self) -> PathBuf {
|
||||
self.storage_path().join("cloud_init_disks")
|
||||
}
|
||||
|
||||
/// Get the path where the disk image of a VM is stored
|
||||
pub fn cloud_init_disk_path_for_vm(&self, name: &str) -> PathBuf {
|
||||
self.cloud_init_disk_storage_path().join(format!(
|
||||
"{}-{name}.iso",
|
||||
constants::CLOUD_INIT_IMAGE_PREFIX_NAME
|
||||
))
|
||||
}
|
||||
|
||||
/// Get disk images storage directory
|
||||
pub fn disk_images_storage_path(&self) -> PathBuf {
|
||||
self.storage_path().join("disk_images")
|
||||
|
@ -30,8 +30,9 @@ pub const ALLOWED_ISO_MIME_TYPES: [&str; 4] = [
|
||||
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
|
||||
|
||||
/// Allowed uploaded disk images formats
|
||||
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 3] = [
|
||||
pub const ALLOWED_DISK_IMAGES_MIME_TYPES: [&str; 4] = [
|
||||
"application/x-qemu-disk",
|
||||
"application/x-raw-disk-image",
|
||||
"application/gzip",
|
||||
"application/octet-stream",
|
||||
];
|
||||
@ -57,6 +58,9 @@ pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50);
|
||||
/// Disk size max (B)
|
||||
pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000);
|
||||
|
||||
/// Cloud init generated disk image prefix
|
||||
pub const CLOUD_INIT_IMAGE_PREFIX_NAME: &str = "virtweb-cloudinit-autogen-image";
|
||||
|
||||
/// Net nat entry comment max size
|
||||
pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250;
|
||||
|
||||
|
@ -55,7 +55,15 @@ pub async fn upload(MultipartForm(mut form): MultipartForm<UploadDiskImageForm>)
|
||||
}
|
||||
|
||||
// Copy the file to the destination
|
||||
file.file.persist(dest_path)?;
|
||||
file.file.persist(&dest_path)?;
|
||||
|
||||
// Check if file information can be loaded
|
||||
if let Err(e) = DiskFileInfo::load_file(&dest_path) {
|
||||
log::error!("Failed to get information about uploaded disk file! {e}");
|
||||
std::fs::remove_file(&dest_path)?;
|
||||
return Ok(HttpResponse::InternalServerError()
|
||||
.json(format!("Unable to process uploaded file! {e}")));
|
||||
}
|
||||
|
||||
Ok(HttpResponse::Ok().json("Successfully uploaded disk image!"))
|
||||
}
|
||||
|
@ -109,6 +109,28 @@ pub async fn get_single_src_def(client: LibVirtReq, id: web::Path<SingleVMUUidRe
|
||||
.body(info))
|
||||
}
|
||||
|
||||
/// Get the generated cloud init configuration disk of a vm
|
||||
pub async fn get_cloud_init_disk(client: LibVirtReq, id: web::Path<SingleVMUUidReq>) -> HttpResult {
|
||||
let info = match client.get_single_domain(id.uid).await {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
log::error!("Failed to get domain information! {e}");
|
||||
return Ok(HttpResponse::InternalServerError().json(e.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let vm = VMInfo::from_domain(info)?;
|
||||
let disk = vm.cloud_init.generate_nocloud_disk()?;
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/x-iso9660-image")
|
||||
.insert_header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"cloud_init_{}.iso\"", vm.name),
|
||||
))
|
||||
.body(disk))
|
||||
}
|
||||
|
||||
/// Update a VM information
|
||||
pub async fn update(
|
||||
client: LibVirtReq,
|
||||
|
@ -142,9 +142,22 @@ impl VMInfo {
|
||||
return Err(StructureExtraction("Invalid number of vCPU specified!").into());
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
let mut iso_absolute_files = vec![];
|
||||
|
||||
// Add ISO files
|
||||
// Process cloud init image
|
||||
if self.cloud_init.attach_config {
|
||||
let cloud_init_disk_path = AppConfig::get().cloud_init_disk_path_for_vm(&self.name);
|
||||
|
||||
// Apply latest cloud init configuration
|
||||
std::fs::write(
|
||||
&cloud_init_disk_path,
|
||||
self.cloud_init.generate_nocloud_disk()?,
|
||||
)?;
|
||||
|
||||
iso_absolute_files.push(cloud_init_disk_path);
|
||||
}
|
||||
|
||||
// Process uploaded 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());
|
||||
@ -156,6 +169,13 @@ impl VMInfo {
|
||||
return Err(StructureExtraction("Specified ISO file does not exists!").into());
|
||||
}
|
||||
|
||||
iso_absolute_files.push(path);
|
||||
}
|
||||
|
||||
let mut disks = vec![];
|
||||
|
||||
// Add ISO disk files
|
||||
for iso_path in iso_absolute_files {
|
||||
disks.push(DiskXML {
|
||||
r#type: "file".to_string(),
|
||||
device: "cdrom".to_string(),
|
||||
@ -165,7 +185,7 @@ impl VMInfo {
|
||||
cache: "none".to_string(),
|
||||
},
|
||||
source: DiskSourceXML {
|
||||
file: path.to_string_lossy().to_string(),
|
||||
file: iso_path.to_string_lossy().to_string(),
|
||||
},
|
||||
target: DiskTargetXML {
|
||||
dev: format!(
|
||||
@ -182,6 +202,7 @@ impl VMInfo {
|
||||
})
|
||||
}
|
||||
|
||||
// Configure VNC access, if requested
|
||||
let (vnc_graphics, vnc_video) = match self.vnc_access {
|
||||
true => (
|
||||
Some(GraphicsXML {
|
||||
@ -495,6 +516,7 @@ impl VMInfo {
|
||||
.iter()
|
||||
.filter(|d| d.device == "cdrom")
|
||||
.map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
|
||||
.filter(|d| !d.starts_with(constants::CLOUD_INIT_IMAGE_PREFIX_NAME))
|
||||
.collect(),
|
||||
|
||||
file_disks: domain
|
||||
|
@ -61,6 +61,8 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
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().cloud_init_disk_storage_path())
|
||||
.unwrap();
|
||||
files_utils::create_directory_if_missing(AppConfig::get().disk_images_storage_path()).unwrap();
|
||||
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_path()).unwrap();
|
||||
files_utils::set_file_permission(AppConfig::get().vnc_sockets_path(), 0o777).unwrap();
|
||||
@ -202,6 +204,10 @@ async fn main() -> std::io::Result<()> {
|
||||
"/api/vm/{uid}/src",
|
||||
web::get().to(vm_controller::get_single_src_def),
|
||||
)
|
||||
.route(
|
||||
"/api/vm/{uid}/cloud_init_disk",
|
||||
web::get().to(vm_controller::get_cloud_init_disk),
|
||||
)
|
||||
.route(
|
||||
"/api/vm/{uid}/autostart",
|
||||
web::get().to(vm_controller::get_autostart),
|
||||
|
@ -1,19 +1,96 @@
|
||||
use crate::app_config::AppConfig;
|
||||
use crate::constants;
|
||||
use std::process::Command;
|
||||
|
||||
/// Cloud init DS Mode
|
||||
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum CloudInitDSMode {
|
||||
/// Networking is required
|
||||
Net,
|
||||
/// Does not require networking to be up before user-data actions are run
|
||||
Local,
|
||||
}
|
||||
|
||||
/// VM Cloud Init configuration
|
||||
///
|
||||
/// RedHat documentation: https://docs.redhat.com/fr/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_cloud-init_for_rhel_9/configuring-cloud-init_cloud-content
|
||||
/// cloud-localds source code: https://github.com/canonical/cloud-utils/blob/main/bin/cloud-localds
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
|
||||
pub struct CloudInitConfig {
|
||||
attach_config: bool,
|
||||
pub attach_config: bool,
|
||||
/// Main user data
|
||||
user_data: String,
|
||||
pub user_data: String,
|
||||
/// Instance ID, set in metadata file
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instance_id: Option<String>,
|
||||
pub instance_id: Option<String>,
|
||||
/// Local hostname, set in metadata file
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
local_hostname: Option<String>,
|
||||
pub local_hostname: Option<String>,
|
||||
/// Data source mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dsmode: Option<CloudInitDSMode>,
|
||||
/// Network configuration
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
network_configuration: Option<String>,
|
||||
pub network_configuration: Option<String>,
|
||||
}
|
||||
|
||||
impl CloudInitConfig {
|
||||
/// Generate disk image for nocloud usage
|
||||
pub fn generate_nocloud_disk(&self) -> anyhow::Result<Vec<u8>> {
|
||||
let temp_path = tempfile::tempdir_in(&AppConfig::get().temp_dir)?;
|
||||
|
||||
let mut cmd = Command::new(constants::PROGRAM_CLOUD_LOCALDS);
|
||||
|
||||
// ISO destination path
|
||||
let temp_iso = temp_path.path().join("disk.iso");
|
||||
cmd.arg(&temp_iso);
|
||||
|
||||
// Process network configuration
|
||||
if let Some(net_conf) = &self.network_configuration {
|
||||
let net_conf_path = temp_path.path().join("network");
|
||||
std::fs::write(&net_conf_path, net_conf)?;
|
||||
cmd.arg("--network-config").arg(&net_conf_path);
|
||||
}
|
||||
|
||||
// Process user data
|
||||
let user_data_path = temp_path.path().join("user-data");
|
||||
std::fs::write(&user_data_path, &self.user_data)?;
|
||||
cmd.arg(user_data_path);
|
||||
|
||||
// Process metadata
|
||||
let mut metadatas = vec![];
|
||||
if let Some(inst_id) = &self.instance_id {
|
||||
metadatas.push(format!("instance-id: {}", inst_id));
|
||||
}
|
||||
if let Some(local_hostname) = &self.local_hostname {
|
||||
metadatas.push(format!("local-hostname: {}", local_hostname));
|
||||
}
|
||||
if let Some(dsmode) = &self.dsmode {
|
||||
metadatas.push(format!(
|
||||
"dsmode: {}",
|
||||
match dsmode {
|
||||
CloudInitDSMode::Net => "net",
|
||||
CloudInitDSMode::Local => "local",
|
||||
}
|
||||
));
|
||||
}
|
||||
let meta_data_path = temp_path.path().join("meta-data");
|
||||
std::fs::write(&meta_data_path, metadatas.join("\n"))?;
|
||||
cmd.arg(meta_data_path);
|
||||
|
||||
// Execute command
|
||||
let output = cmd.output()?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"{} exited with status {}!\nStdout: {}\nStderr: {}",
|
||||
constants::PROGRAM_CLOUD_LOCALDS,
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
// Read generated ISO file
|
||||
Ok(std::fs::read(temp_iso)?)
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,15 @@ export interface VMNetBridge {
|
||||
bridge: string;
|
||||
}
|
||||
|
||||
export interface VMCloudInit {
|
||||
attach_config: boolean;
|
||||
user_data: string;
|
||||
instance_id?: string;
|
||||
local_hostname?: string;
|
||||
dsmode?: "Net" | "Local";
|
||||
network_configuration?: string;
|
||||
}
|
||||
|
||||
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
|
||||
|
||||
interface VMInfoInterface {
|
||||
@ -101,6 +110,7 @@ interface VMInfoInterface {
|
||||
networks: VMNetInterface[];
|
||||
tpm_module: boolean;
|
||||
oem_strings: string[];
|
||||
cloud_init: VMCloudInit;
|
||||
}
|
||||
|
||||
export class VMInfo implements VMInfoInterface {
|
||||
@ -120,6 +130,7 @@ export class VMInfo implements VMInfoInterface {
|
||||
networks: VMNetInterface[];
|
||||
tpm_module: boolean;
|
||||
oem_strings: string[];
|
||||
cloud_init: VMCloudInit;
|
||||
|
||||
constructor(int: VMInfoInterface) {
|
||||
this.name = int.name;
|
||||
@ -138,6 +149,7 @@ export class VMInfo implements VMInfoInterface {
|
||||
this.networks = int.networks;
|
||||
this.tpm_module = int.tpm_module;
|
||||
this.oem_strings = int.oem_strings;
|
||||
this.cloud_init = int.cloud_init;
|
||||
}
|
||||
|
||||
static NewEmpty(): VMInfo {
|
||||
@ -153,6 +165,7 @@ export class VMInfo implements VMInfoInterface {
|
||||
networks: [],
|
||||
tpm_module: true,
|
||||
oem_strings: [],
|
||||
cloud_init: { attach_config: false, user_data: "" },
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,7 @@ export function TokenRightsEditor(p: {
|
||||
<TableCell align="center">Get XML definition</TableCell>
|
||||
<TableCell align="center">Get autostart</TableCell>
|
||||
<TableCell align="center">Set autostart</TableCell>
|
||||
<TableCell align="center">Get CloudInit disk</TableCell>
|
||||
<TableCell align="center">Backup disk</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@ -84,6 +85,13 @@ export function TokenRightsEditor(p: {
|
||||
{...p}
|
||||
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
||||
/>
|
||||
<CellRight
|
||||
{...p}
|
||||
right={{
|
||||
verb: "GET",
|
||||
path: "/api/vm/*/cloud_init_disk",
|
||||
}}
|
||||
/>
|
||||
<CellRight
|
||||
{...p}
|
||||
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
|
||||
@ -123,7 +131,15 @@ export function TokenRightsEditor(p: {
|
||||
{...p}
|
||||
right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
|
||||
parent={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
||||
/>{" "}
|
||||
/>
|
||||
<CellRight
|
||||
{...p}
|
||||
right={{
|
||||
verb: "GET",
|
||||
path: `/api/vm/${v.uuid}/cloud_init_disk`,
|
||||
}}
|
||||
parent={{ verb: "GET", path: "/api/vm/*/cloud_init_disk" }}
|
||||
/>
|
||||
<CellRight
|
||||
{...p}
|
||||
right={{
|
||||
|
Reference in New Issue
Block a user