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:
311
src/bin/configure_iscsi.rs
Normal file
311
src/bin/configure_iscsi.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user