use crate::brand::bhyve::bhyve_install_args; use crate::brand::lx::lx_install_args; use crate::command::{exec, CommandOutput}; use crate::error::Result; use crate::storage::StorageEngine; use crate::traits::ZoneRuntime; use crate::types::*; use crate::zone::config::generate_zonecfg; use crate::zone::state::parse_zoneadm_line; use async_trait::async_trait; use std::sync::Arc; use tracing::info; /// illumos zone runtime implementation /// /// Manages real zones via zonecfg/zoneadm, dladm for networking. /// Storage (ZFS datasets) is delegated to the injected `StorageEngine`. pub struct IllumosRuntime { storage: Arc, } impl IllumosRuntime { pub fn new(storage: Arc) -> Self { Self { storage } } } #[async_trait] impl ZoneRuntime for IllumosRuntime { async fn create_zone(&self, config: &ZoneConfig) -> Result<()> { info!("Creating zone: {}", config.zone_name); let zonecfg_content = generate_zonecfg(config)?; // Write config to a temp file, then apply via zonecfg let tmp_path = format!("/tmp/zonecfg-{}.cmd", config.zone_name); tokio::fs::write(&tmp_path, &zonecfg_content) .await .map_err(|e| { crate::error::RuntimeError::zone_operation_failed(&config.zone_name, e.to_string()) })?; let result = exec("zonecfg", &["-z", &config.zone_name, "-f", &tmp_path]).await; // Clean up temp file (best-effort) let _ = tokio::fs::remove_file(&tmp_path).await; result?; info!("Zone configured: {}", config.zone_name); Ok(()) } async fn install_zone(&self, zone_name: &str) -> Result<()> { info!("Installing zone: {}", zone_name); exec("zoneadm", &["-z", zone_name, "install"]).await?; info!("Zone installed: {}", zone_name); Ok(()) } async fn boot_zone(&self, zone_name: &str) -> Result<()> { info!("Booting zone: {}", zone_name); exec("zoneadm", &["-z", zone_name, "boot"]).await?; info!("Zone booted: {}", zone_name); Ok(()) } async fn shutdown_zone(&self, zone_name: &str) -> Result<()> { info!("Shutting down zone: {}", zone_name); exec("zoneadm", &["-z", zone_name, "shutdown"]).await?; info!("Zone shutdown: {}", zone_name); Ok(()) } async fn halt_zone(&self, zone_name: &str) -> Result<()> { info!("Halting zone: {}", zone_name); exec("zoneadm", &["-z", zone_name, "halt"]).await?; info!("Zone halted: {}", zone_name); Ok(()) } async fn uninstall_zone(&self, zone_name: &str) -> Result<()> { info!("Uninstalling zone: {}", zone_name); exec("zoneadm", &["-z", zone_name, "uninstall", "-F"]).await?; info!("Zone uninstalled: {}", zone_name); Ok(()) } async fn delete_zone(&self, zone_name: &str) -> Result<()> { info!("Deleting zone: {}", zone_name); exec("zonecfg", &["-z", zone_name, "delete", "-F"]).await?; info!("Zone deleted: {}", zone_name); Ok(()) } async fn exec_in_zone(&self, zone_name: &str, command: &[String]) -> Result { let mut args: Vec<&str> = vec![zone_name]; let str_refs: Vec<&str> = command.iter().map(|s| s.as_str()).collect(); args.extend(str_refs); crate::command::exec_unchecked("zlogin", &args).await } async fn get_zone_state(&self, zone_name: &str) -> Result { let output = exec("zoneadm", &["-z", zone_name, "list", "-p"]).await?; let line = output.stdout.trim(); let info = parse_zoneadm_line(line)?; Ok(info.state) } async fn get_zone_info(&self, zone_name: &str) -> Result { let output = exec("zoneadm", &["-z", zone_name, "list", "-cp"]).await?; let line = output.stdout.trim(); parse_zoneadm_line(line) } async fn list_zones(&self) -> Result> { let output = exec("zoneadm", &["list", "-cp"]).await?; let mut zones = Vec::new(); for line in output.stdout.lines() { let line = line.trim(); if line.is_empty() { continue; } let info = parse_zoneadm_line(line)?; // Filter out the global zone if info.zone_name == "global" { continue; } zones.push(info); } Ok(zones) } async fn setup_network(&self, zone_name: &str, network: &NetworkMode) -> Result<()> { info!("Setting up network for zone: {}", zone_name); match network { NetworkMode::Etherstub(cfg) => { // Create etherstub (ignore if already exists) let _ = exec("dladm", &["create-etherstub", &cfg.etherstub_name]).await; // Create VNIC on etherstub exec( "dladm", &["create-vnic", "-l", &cfg.etherstub_name, &cfg.vnic_name], ) .await?; } NetworkMode::Direct(cfg) => { // Create VNIC directly on physical NIC exec( "dladm", &["create-vnic", "-l", &cfg.physical_nic, &cfg.vnic_name], ) .await?; } } info!("Network setup complete for zone: {}", zone_name); Ok(()) } async fn configure_zone_ip(&self, zone_name: &str, network: &NetworkMode) -> Result<()> { let (vnic_name, ip_address, prefix_len, gateway) = match network { NetworkMode::Etherstub(cfg) => ( &cfg.vnic_name, &cfg.ip_address, cfg.prefix_len, &cfg.gateway, ), NetworkMode::Direct(cfg) => ( &cfg.vnic_name, &cfg.ip_address, cfg.prefix_len, &cfg.gateway, ), }; info!( "Configuring IP {} on {} in zone {}", ip_address, vnic_name, zone_name ); // Create the IP interface self.exec_in_zone( zone_name, &[ "ipadm".to_string(), "create-if".to_string(), "-t".to_string(), vnic_name.clone(), ], ) .await .map_err(|e| RuntimeError::network_error(format!("ipadm create-if failed: {}", e)))?; // Assign a static IP address self.exec_in_zone( zone_name, &[ "ipadm".to_string(), "create-addr".to_string(), "-T".to_string(), "static".to_string(), "-a".to_string(), format!("{}/{}", ip_address, prefix_len), format!("{}/v4", vnic_name), ], ) .await .map_err(|e| RuntimeError::network_error(format!("ipadm create-addr failed: {}", e)))?; // Add default route self.exec_in_zone( zone_name, &[ "route".to_string(), "-p".to_string(), "add".to_string(), "default".to_string(), gateway.clone(), ], ) .await .map_err(|e| RuntimeError::network_error(format!("route add default failed: {}", e)))?; info!("IP configuration complete for zone: {}", zone_name); Ok(()) } async fn teardown_network(&self, zone_name: &str, network: &NetworkMode) -> Result<()> { info!("Tearing down network for zone: {}", zone_name); let vnic_name = match network { NetworkMode::Etherstub(cfg) => &cfg.vnic_name, NetworkMode::Direct(cfg) => &cfg.vnic_name, }; exec("dladm", &["delete-vnic", vnic_name]).await?; info!("Network teardown complete for zone: {}", zone_name); Ok(()) } async fn provision(&self, config: &ZoneConfig) -> Result<()> { info!("Provisioning zone: {}", config.zone_name); self.storage .create_zone_dataset(&config.zone_name, &config.storage) .await?; self.setup_network(&config.zone_name, &config.network) .await?; self.create_zone(config).await?; // Brand-specific install match config.brand { ZoneBrand::Lx => { let args = lx_install_args(config)?; let mut cmd_args = vec!["-z", &config.zone_name, "install"]; let str_args: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); cmd_args.extend(str_args); exec("zoneadm", &cmd_args).await?; } ZoneBrand::Bhyve => { let args = bhyve_install_args(config)?; let mut cmd_args = vec!["-z", &config.zone_name, "install"]; let str_args: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); cmd_args.extend(str_args); exec("zoneadm", &cmd_args).await?; } ZoneBrand::Reddwarf => { self.install_zone(&config.zone_name).await?; } } self.boot_zone(&config.zone_name).await?; // Brief pause to let the zone finish booting before configuring IP tokio::time::sleep(std::time::Duration::from_secs(1)).await; self.configure_zone_ip(&config.zone_name, &config.network) .await?; info!("Zone provisioned: {}", config.zone_name); Ok(()) } async fn deprovision(&self, config: &ZoneConfig) -> Result<()> { info!("Deprovisioning zone: {}", config.zone_name); // Best-effort halt (may fail if already not running) let _ = self.halt_zone(&config.zone_name).await; self.uninstall_zone(&config.zone_name).await?; self.delete_zone(&config.zone_name).await?; self.teardown_network(&config.zone_name, &config.network) .await?; self.storage.destroy_zone_dataset(&config.zone_name).await?; info!("Zone deprovisioned: {}", config.zone_name); Ok(()) } }