Recreate configure_iscsi.rs after accidental overwrite

- Fixed IscsiConfig struct with SQLite LUN mapping (310 lines)
- Added 6 unit tests (all passing)
- Replaced structopt with clap Parser
- Tests: 37/38 passed (96%)
- WebDAV server verified: warren.sqlite (12659 nodes)
- Release binary: 2.7MB
This commit is contained in:
Warren
2026-05-18 01:46:54 +08:00
parent 3cfc5eeb54
commit d646e81e36

311
src/bin/configure_iscsi.rs Normal file
View File

@@ -0,0 +1,311 @@
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<String> = 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<String> {
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<Option<String>> {
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<u64> {
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<String>,
#[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);
}
}