use anyhow::Result; use clap::Parser; use rusqlite::Connection; use std::path::PathBuf; use std::process::Command; #[derive(Debug, Clone)] pub struct IscsiConfig { pub raid_device: String, pub target_iqn: String, pub portal_ip: String, pub portal_port: u16, pub db_path: PathBuf, } impl IscsiConfig { pub fn new(user_id: &str) -> Self { let raid_device = format!("/dev/mapper/markbase_{}", user_id); let target_iqn = format!("iqn.2026-05.momentry:markbase_{}", user_id); let db_path = PathBuf::from(format!("data/users/{}/{}.sqlite", user_id, user_id)); Self { raid_device, target_iqn, portal_ip: "0.0.0.0".to_string(), portal_port: 3260, db_path, } } pub fn create_raid5(&self, disks: &[String], stripe_size_kb: u64) -> Result<()> { if disks.len() < 3 { return Err(anyhow::anyhow!("RAID5 requires at least 3 disks")); } let _disk_args: Vec = disks.iter().map(|d| d.clone()).collect(); let output = Command::new("dmsetup") .arg("create") .arg(&self.raid_device.replace("/dev/mapper/", "")) .arg("--table") .arg(format!( "0 {} raid raid5 {} {} region_size {}", get_disk_size(&disks[0])?, disks.len(), stripe_size_kb * 1024, stripe_size_kb )) .output()?; if !output.status.success() { return Err(anyhow::anyhow!("dmsetup failed: {}", String::from_utf8_lossy(&output.stderr))); } println!("RAID5 created: {}", self.raid_device); Ok(()) } pub fn verify_raid(&self) -> Result { let output = Command::new("dmsetup") .arg("status") .arg(&self.raid_device.replace("/dev/mapper/", "")) .output()?; let status = String::from_utf8_lossy(&output.stdout).trim().to_string(); println!("RAID5 status: {}", status); Ok(status) } pub fn create_iscsi_target(&self) -> Result<()> { let targetcli_commands = [ format!("cd backstores/block"), format!("create name={} dev={}", self.raid_device.replace("/dev/mapper/", "markbase_"), self.raid_device), format!("cd /iscsi"), format!("create {}", self.target_iqn), format!("cd {}/{}/tpg1/luns", "/iscsi", self.target_iqn), format!("create /backstores/block/markbase_{}", self.raid_device.replace("/dev/mapper/", "")), format!("cd {}/{}/tpg1/portals", "/iscsi", self.target_iqn), format!("create {} {}", self.portal_ip, self.portal_port), ]; for cmd in &targetcli_commands { let output = Command::new("targetcli") .arg(cmd) .output()?; if !output.status.success() { return Err(anyhow::anyhow!("targetcli failed: {}", String::from_utf8_lossy(&output.stderr))); } } println!("iSCSI Target created: {}", self.target_iqn); println!("Portal: {}:{}", self.portal_ip, self.portal_port); Ok(()) } pub fn init_db(&self) -> Result<()> { if !self.db_path.exists() { std::fs::create_dir_all(self.db_path.parent().unwrap())?; let conn = Connection::open(&self.db_path)?; conn.execute( "CREATE TABLE IF NOT EXISTS lun_mapping ( lun INTEGER PRIMARY KEY, node_id TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) )", [], )?; conn.execute( "CREATE INDEX IF NOT EXISTS idx_node_id ON lun_mapping(node_id)", [], )?; println!("Database created: {}", self.db_path.display()); } else { println!("Database exists: {}", self.db_path.display()); } Ok(()) } pub fn map_lun_to_sqlite(&self, lun: u64, node_id: &str) -> Result<()> { let conn = Connection::open(&self.db_path)?; conn.execute( "INSERT OR REPLACE INTO lun_mapping (lun, node_id) VALUES (?1, ?2)", rusqlite::params![lun, node_id], )?; println!("Mapped LUN {} -> node_id {}", lun, node_id); Ok(()) } pub fn get_node_id_for_lun(&self, lun: u64) -> Result> { let conn = Connection::open(&self.db_path)?; let result = conn.query_row( "SELECT node_id FROM lun_mapping WHERE lun = ?1", rusqlite::params![lun], |row| row.get::<_, String>(0), ); match result { Ok(node_id) => Ok(Some(node_id)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), Err(e) => Err(anyhow::anyhow!("Database error: {}", e)), } } } fn get_disk_size(disk_path: &str) -> Result { let output = Command::new("blockdev") .arg("--getsize64") .arg(disk_path) .output()?; let size_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); let size: u64 = size_str.parse()?; Ok(size) } #[derive(Parser, Debug)] #[command(name = "configure_iscsi", about = "MarkBase iSCSI configuration tool")] struct Opt { #[arg(help = "User ID")] user_id: String, #[arg(long, help = "Disks for RAID5 (minimum 3)")] disks: Vec, #[arg(long, default_value = "64", help = "Stripe size in KB")] stripe_size: u64, #[arg(long, help = "Verify existing configuration")] verify: bool, #[arg(long, help = "Create iSCSI target only")] create_target: bool, } fn main() -> Result<()> { let opt = Opt::parse(); let config = IscsiConfig::new(&opt.user_id); println!("=== MarkBase iSCSI Configuration ==="); println!("User ID: {}", opt.user_id); if opt.verify { println!("Verifying existing configuration..."); config.verify_raid()?; println!("Configuration verified successfully"); return Ok(()); } if !opt.disks.is_empty() { println!("Creating RAID5 with disks: {:?}", opt.disks); config.create_raid5(&opt.disks, opt.stripe_size)?; config.verify_raid()?; } if opt.create_target { println!("Creating iSCSI target..."); config.create_iscsi_target()?; } config.init_db()?; println!("=== Configuration Complete ==="); Ok(()) } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; use rusqlite::Connection; #[test] fn test_iscsi_config_creation() { let config = IscsiConfig::new("test_user"); assert_eq!(config.raid_device, "/dev/mapper/markbase_test_user"); assert_eq!(config.target_iqn, "iqn.2026-05.momentry:markbase_test_user"); assert_eq!(config.portal_ip, "0.0.0.0"); assert_eq!(config.portal_port, 3260); } #[test] fn test_db_path_format() { let config = IscsiConfig::new("warren"); assert!(config.db_path.to_str().unwrap().contains("warren.sqlite")); } #[test] fn test_target_iqn_format() { let user_ids = ["warren", "momentry", "demo"]; for user_id in user_ids { let config = IscsiConfig::new(user_id); assert!(config.target_iqn.starts_with("iqn.2026-05.momentry:markbase_")); assert!(config.target_iqn.ends_with(user_id)); } } #[test] fn test_raid_device_format() { let config = IscsiConfig::new("test123"); assert!(config.raid_device.starts_with("/dev/mapper/")); assert!(config.raid_device.ends_with("test123")); } #[test] fn test_sqlite_lun_mapping() { let temp_dir = TempDir::new().unwrap(); let db_path = temp_dir.path().join("test.sqlite"); let config = IscsiConfig::new("test_user"); let config_with_db = IscsiConfig { db_path: db_path.clone(), ..config }; config_with_db.init_db().unwrap(); let result = config_with_db.map_lun_to_sqlite(1, "node_abc123"); assert!(result.is_ok()); let conn = Connection::open(&db_path).unwrap(); let node_id: String = conn.query_row( "SELECT node_id FROM lun_mapping WHERE lun = 1", [], |row| row.get(0) ).unwrap(); assert_eq!(node_id, "node_abc123"); } #[test] fn test_multiple_lun_mappings() { let temp_dir = TempDir::new().unwrap(); let db_path = temp_dir.path().join("test_multi.sqlite"); let config = IscsiConfig::new("test_user"); let config_with_db = IscsiConfig { db_path: db_path.clone(), ..config }; config_with_db.init_db().unwrap(); for i in 1..=10 { let result = config_with_db.map_lun_to_sqlite(i, &format!("node_{}", i)); assert!(result.is_ok()); } let conn = Connection::open(&db_path).unwrap(); let count: i64 = conn.query_row( "SELECT COUNT(*) FROM lun_mapping", [], |row| row.get(0) ).unwrap(); assert_eq!(count, 10); } }