Compare commits
5 Commits
33d70c2ee0
...
3680ceed61
Author | SHA1 | Date | |
---|---|---|---|
3680ceed61 | |||
8a7712ec42 | |||
9609cfb33a | |||
1fe7c60f36 | |||
f1339f0711 |
@ -182,6 +182,13 @@ impl Handler<DeleteDomainReq> for LibVirtActor {
|
|||||||
false => sys::VIR_DOMAIN_UNDEFINE_NVRAM,
|
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 {
|
if !msg.keep_files {
|
||||||
log::info!("Delete storage associated with the domain");
|
log::info!("Delete storage associated with the domain");
|
||||||
let path = AppConfig::get().vm_storage_path(msg.id);
|
let path = AppConfig::get().vm_storage_path(msg.id);
|
||||||
|
@ -250,6 +250,19 @@ impl AppConfig {
|
|||||||
self.storage_path().join("iso")
|
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
|
/// Get disk images storage directory
|
||||||
pub fn disk_images_storage_path(&self) -> PathBuf {
|
pub fn disk_images_storage_path(&self) -> PathBuf {
|
||||||
self.storage_path().join("disk_images")
|
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);
|
pub const ISO_MAX_SIZE: FileSize = FileSize::from_gb(10);
|
||||||
|
|
||||||
/// Allowed uploaded disk images formats
|
/// 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-qemu-disk",
|
||||||
|
"application/x-raw-disk-image",
|
||||||
"application/gzip",
|
"application/gzip",
|
||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
];
|
];
|
||||||
@ -57,6 +58,9 @@ pub const DISK_SIZE_MIN: FileSize = FileSize::from_mb(50);
|
|||||||
/// Disk size max (B)
|
/// Disk size max (B)
|
||||||
pub const DISK_SIZE_MAX: FileSize = FileSize::from_gb(20000);
|
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
|
/// Net nat entry comment max size
|
||||||
pub const NET_NAT_COMMENT_MAX_SIZE: usize = 250;
|
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
|
// 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!"))
|
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))
|
.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
|
/// Update a VM information
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
client: LibVirtReq,
|
client: LibVirtReq,
|
||||||
|
@ -142,9 +142,22 @@ impl VMInfo {
|
|||||||
return Err(StructureExtraction("Invalid number of vCPU specified!").into());
|
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 {
|
for iso_file in &self.iso_files {
|
||||||
if !files_utils::check_file_name(iso_file) {
|
if !files_utils::check_file_name(iso_file) {
|
||||||
return Err(StructureExtraction("ISO filename is invalid!").into());
|
return Err(StructureExtraction("ISO filename is invalid!").into());
|
||||||
@ -156,6 +169,13 @@ impl VMInfo {
|
|||||||
return Err(StructureExtraction("Specified ISO file does not exists!").into());
|
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 {
|
disks.push(DiskXML {
|
||||||
r#type: "file".to_string(),
|
r#type: "file".to_string(),
|
||||||
device: "cdrom".to_string(),
|
device: "cdrom".to_string(),
|
||||||
@ -165,7 +185,7 @@ impl VMInfo {
|
|||||||
cache: "none".to_string(),
|
cache: "none".to_string(),
|
||||||
},
|
},
|
||||||
source: DiskSourceXML {
|
source: DiskSourceXML {
|
||||||
file: path.to_string_lossy().to_string(),
|
file: iso_path.to_string_lossy().to_string(),
|
||||||
},
|
},
|
||||||
target: DiskTargetXML {
|
target: DiskTargetXML {
|
||||||
dev: format!(
|
dev: format!(
|
||||||
@ -182,6 +202,7 @@ impl VMInfo {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure VNC access, if requested
|
||||||
let (vnc_graphics, vnc_video) = match self.vnc_access {
|
let (vnc_graphics, vnc_video) = match self.vnc_access {
|
||||||
true => (
|
true => (
|
||||||
Some(GraphicsXML {
|
Some(GraphicsXML {
|
||||||
@ -495,6 +516,7 @@ impl VMInfo {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.device == "cdrom")
|
.filter(|d| d.device == "cdrom")
|
||||||
.map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
|
.map(|d| d.source.file.rsplit_once('/').unwrap().1.to_string())
|
||||||
|
.filter(|d| !d.starts_with(constants::CLOUD_INIT_IMAGE_PREFIX_NAME))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
||||||
file_disks: domain
|
file_disks: domain
|
||||||
|
@ -61,6 +61,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
log::debug!("Create required directory, if missing");
|
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().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().disk_images_storage_path()).unwrap();
|
||||||
files_utils::create_directory_if_missing(AppConfig::get().vnc_sockets_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();
|
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",
|
"/api/vm/{uid}/src",
|
||||||
web::get().to(vm_controller::get_single_src_def),
|
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(
|
.route(
|
||||||
"/api/vm/{uid}/autostart",
|
"/api/vm/{uid}/autostart",
|
||||||
web::get().to(vm_controller::get_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
|
/// 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
|
/// 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
|
/// cloud-localds source code: https://github.com/canonical/cloud-utils/blob/main/bin/cloud-localds
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Default)]
|
||||||
pub struct CloudInitConfig {
|
pub struct CloudInitConfig {
|
||||||
attach_config: bool,
|
pub attach_config: bool,
|
||||||
/// Main user data
|
/// Main user data
|
||||||
user_data: String,
|
pub user_data: String,
|
||||||
/// Instance ID, set in metadata file
|
/// Instance ID, set in metadata file
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
instance_id: Option<String>,
|
pub instance_id: Option<String>,
|
||||||
/// Local hostname, set in metadata file
|
/// Local hostname, set in metadata file
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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
|
/// Network configuration
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[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;
|
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";
|
export type VMBootType = "UEFI" | "UEFISecureBoot" | "Legacy";
|
||||||
|
|
||||||
interface VMInfoInterface {
|
interface VMInfoInterface {
|
||||||
@ -101,6 +110,7 @@ interface VMInfoInterface {
|
|||||||
networks: VMNetInterface[];
|
networks: VMNetInterface[];
|
||||||
tpm_module: boolean;
|
tpm_module: boolean;
|
||||||
oem_strings: string[];
|
oem_strings: string[];
|
||||||
|
cloud_init: VMCloudInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VMInfo implements VMInfoInterface {
|
export class VMInfo implements VMInfoInterface {
|
||||||
@ -120,6 +130,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
networks: VMNetInterface[];
|
networks: VMNetInterface[];
|
||||||
tpm_module: boolean;
|
tpm_module: boolean;
|
||||||
oem_strings: string[];
|
oem_strings: string[];
|
||||||
|
cloud_init: VMCloudInit;
|
||||||
|
|
||||||
constructor(int: VMInfoInterface) {
|
constructor(int: VMInfoInterface) {
|
||||||
this.name = int.name;
|
this.name = int.name;
|
||||||
@ -138,6 +149,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
this.networks = int.networks;
|
this.networks = int.networks;
|
||||||
this.tpm_module = int.tpm_module;
|
this.tpm_module = int.tpm_module;
|
||||||
this.oem_strings = int.oem_strings;
|
this.oem_strings = int.oem_strings;
|
||||||
|
this.cloud_init = int.cloud_init;
|
||||||
}
|
}
|
||||||
|
|
||||||
static NewEmpty(): VMInfo {
|
static NewEmpty(): VMInfo {
|
||||||
@ -153,6 +165,7 @@ export class VMInfo implements VMInfoInterface {
|
|||||||
networks: [],
|
networks: [],
|
||||||
tpm_module: true,
|
tpm_module: true,
|
||||||
oem_strings: [],
|
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 XML definition</TableCell>
|
||||||
<TableCell align="center">Get autostart</TableCell>
|
<TableCell align="center">Get autostart</TableCell>
|
||||||
<TableCell align="center">Set autostart</TableCell>
|
<TableCell align="center">Set autostart</TableCell>
|
||||||
|
<TableCell align="center">Get CloudInit disk</TableCell>
|
||||||
<TableCell align="center">Backup disk</TableCell>
|
<TableCell align="center">Backup disk</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@ -84,6 +85,13 @@ export function TokenRightsEditor(p: {
|
|||||||
{...p}
|
{...p}
|
||||||
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
|
||||||
/>
|
/>
|
||||||
|
<CellRight
|
||||||
|
{...p}
|
||||||
|
right={{
|
||||||
|
verb: "GET",
|
||||||
|
path: "/api/vm/*/cloud_init_disk",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<CellRight
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
|
right={{ verb: "POST", path: "/api/vm/*/disk/*/backup" }}
|
||||||
@ -123,7 +131,15 @@ export function TokenRightsEditor(p: {
|
|||||||
{...p}
|
{...p}
|
||||||
right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
|
right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
|
||||||
parent={{ verb: "PUT", path: "/api/vm/*/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
|
<CellRight
|
||||||
{...p}
|
{...p}
|
||||||
right={{
|
right={{
|
||||||
|
Reference in New Issue
Block a user