mirror of
https://github.com/CloudNebulaProject/reddwarf.git
synced 2026-04-10 13:20:40 +00:00
Service networking:
- ClusterIP IPAM allocation on service create/delete via reusable Ipam with_prefix()
- ServiceController watches Pod/Service events + periodic reconcile to track endpoints
- NatManager generates ipnat rdr rules for ClusterIP -> pod IP forwarding
- Embedded DNS server resolves {svc}.{ns}.svc.cluster.local to ClusterIP
- New CLI flags: --service-cidr (default 10.96.0.0/12), --cluster-dns (default 0.0.0.0:10053)
Quick wins:
- ipadm IP assignment: configure_zone_ip() runs ipadm/route inside zone via zlogin after boot
- Node heartbeat zone state reporting: reddwarf.io/zone-count and zone-summary annotations
- bhyve brand support: ZoneBrand::Bhyve, install args, zonecfg device generation, controller integration
189 tests passing, clippy clean.
https://claude.ai/code/session_016QLFjAyYGzMPbBjEGMe75j
302 lines
10 KiB
Rust
302 lines
10 KiB
Rust
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<dyn StorageEngine>,
|
|
}
|
|
|
|
impl IllumosRuntime {
|
|
pub fn new(storage: Arc<dyn StorageEngine>) -> 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<CommandOutput> {
|
|
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<ZoneState> {
|
|
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<ZoneInfo> {
|
|
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<Vec<ZoneInfo>> {
|
|
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(())
|
|
}
|
|
}
|