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 { pub attach_config: bool, /// Main user data pub user_data: String, /// Instance ID, set in metadata file #[serde(skip_serializing_if = "Option::is_none")] pub instance_id: Option, /// Local hostname, set in metadata file #[serde(skip_serializing_if = "Option::is_none")] pub local_hostname: Option, /// Data source mode #[serde(skip_serializing_if = "Option::is_none")] pub dsmode: Option, /// Network configuration #[serde(skip_serializing_if = "Option::is_none")] pub network_configuration: Option, } impl CloudInitConfig { /// Check cloud init configuration pub fn check_error(&self) -> Option { if !self.user_data.is_empty() { // Check YAML content if let Err(e) = serde_yml::from_str::(&self.user_data) { return Some(format!( "user data is an invalid YAML file! Deserialization error: {e}" )); } // Check first line if !self.user_data.starts_with("#cloud-config\n") { return Some( "user data file MUST start with '#cloud-config' as first line!".to_string(), ); } } None } /// Generate disk image for nocloud usage pub fn generate_nocloud_disk(&self) -> anyhow::Result> { 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)?) } }