MarkBase架构升级:Multi-Volume Virtual Tree + Dual-View Management + Git Remote修正
核心功能: - ✅ Categories/Series双视图管理(category_view.rs + import_markdown.rs) - ✅ FUSE Multi-Volume支持(tree_type参数) - ✅ SSH/SFTP/SCP/rsync协议完整实现(4042行) - ✅ NFS/SMB Module Phase 1-3完成 - ✅ Archive Module Phase 1-4完成(2916行) - ✅ Download Center API完整实现 - ✅ S3兼容API实现(560行) Git配置修正: - ✅ 删除错误origin(gitea.momentry.ddns.net) - ✅ 删除m5max128(指向机器名) - ✅ 设置origin = m5max128gitea.momentry.ddns.net/admin/markbase - ✅ 设置m4minigitea = m4minigitea.momentry.ddns.net/warren/markbase 数据清理: - ✅ 删除38个临时SQLite(保留accusys.sqlite、demo.sqlite) - ✅ 删除.bak、test_*.bin、调试脚本等临时文件 - ✅ 删除临时目录(build/、download files/、raid_test/等) - ✅ 更新.gitignore排除临时文件 架构优化: - 52个文件修改,2434行新增,4739行删除 - Workspace成员整合(16个crate) - 数据库状态:accusys.sqlite保留(主demo测试) 远程同步: - ✅ 准备推送到m5max128gitea(远程Gitea) - ✅ 准备推送到m4minigitea(本地Gitea)
This commit is contained in:
59
rust-iscsi-initiator/src/bin/iscsi-inq.rs
Normal file
59
rust-iscsi-initiator/src/bin/iscsi-inq.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use rust_iscsi_initiator::connection::IscsiConnection;
|
||||
use rust_iscsi_initiator::tools::inquiry;
|
||||
use std::env;
|
||||
|
||||
/// iscsi-inq tool - Inquiry SCSI device
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("Usage: iscsi-inq <iscsi-url>");
|
||||
println!("Example: iscsi-inq iscsi://192.168.1.1:3260/iqn.target/0");
|
||||
return;
|
||||
}
|
||||
|
||||
let url = &args[1];
|
||||
|
||||
println!("Inquiring device: {}", url);
|
||||
|
||||
// Parse URL (simplified)
|
||||
let parts: Vec<&str> = url.split('/').collect();
|
||||
if parts.len() < 5 {
|
||||
eprintln!("Invalid URL format");
|
||||
return;
|
||||
}
|
||||
|
||||
let portal = parts[2];
|
||||
let target = parts[3];
|
||||
let lun: u64 = parts[4].parse().unwrap_or(0);
|
||||
|
||||
match IscsiConnection::connect(portal).await {
|
||||
Ok(mut conn) => {
|
||||
match conn.login("iqn.initiator", target).await {
|
||||
Ok(_) => match inquiry(&mut conn, lun).await {
|
||||
Ok(inquiry) => {
|
||||
println!("Device Information:");
|
||||
println!(" Type: {}", inquiry.peripheral_type);
|
||||
println!(" Vendor: {}", inquiry.vendor_id);
|
||||
println!(" Product: {}", inquiry.product_id);
|
||||
println!(" Revision: {}", inquiry.product_rev);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Inquiry error: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Login error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
conn.close().await.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Connection error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
rust-iscsi-initiator/src/bin/iscsi-ls.rs
Normal file
47
rust-iscsi-initiator/src/bin/iscsi-ls.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use rust_iscsi_initiator::discovery::Discovery;
|
||||
use std::env;
|
||||
|
||||
/// iscsi-ls tool - List iSCSI targets
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("Usage: iscsi-ls <portal-address>");
|
||||
println!("Example: iscsi-ls 192.168.1.1:3260");
|
||||
return;
|
||||
}
|
||||
|
||||
let portal = &args[1];
|
||||
|
||||
println!("Discovering targets at {}...", portal);
|
||||
|
||||
let mut discovery = Discovery::new();
|
||||
|
||||
match discovery.connect(portal).await {
|
||||
Ok(_) => {
|
||||
match discovery.send_targets().await {
|
||||
Ok(targets) => {
|
||||
if targets.is_empty() {
|
||||
println!("No targets found");
|
||||
} else {
|
||||
println!("Found {} targets:", targets.len());
|
||||
for target in targets {
|
||||
println!(" {}", target);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Discovery error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
discovery.disconnect().await.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Connection error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
rust-iscsi-initiator/src/bin/iscsi-perf.rs
Normal file
79
rust-iscsi-initiator/src/bin/iscsi-perf.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use rust_iscsi_initiator::connection::IscsiConnection;
|
||||
use rust_iscsi_initiator::pdu::{IscsiPdu, Opcode};
|
||||
use rust_iscsi_initiator::scsi::ScsiCommand;
|
||||
use std::env;
|
||||
use std::time::Instant;
|
||||
|
||||
/// iscsi-perf tool - Performance test
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
println!("Usage: iscsi-perf <iscsi-url>");
|
||||
println!("Example: iscsi-perf iscsi://192.168.1.1:3260/iqn.target/0");
|
||||
return;
|
||||
}
|
||||
|
||||
let url = &args[1];
|
||||
|
||||
println!("Performance test: {}", url);
|
||||
|
||||
// Parse URL
|
||||
let parts: Vec<&str> = url.split('/').collect();
|
||||
if parts.len() < 5 {
|
||||
eprintln!("Invalid URL format");
|
||||
return;
|
||||
}
|
||||
|
||||
let portal = parts[2];
|
||||
let target = parts[3];
|
||||
let lun: u64 = parts[4].parse().unwrap_or(0);
|
||||
|
||||
match IscsiConnection::connect(portal).await {
|
||||
Ok(mut conn) => {
|
||||
match conn.login("iqn.initiator", target).await {
|
||||
Ok(_) => {
|
||||
println!("Connected, testing performance...");
|
||||
|
||||
// Test read performance
|
||||
let iterations = 100;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..iterations {
|
||||
let cmd = ScsiCommand::Read10 {
|
||||
lba: i,
|
||||
transfer_length: 1,
|
||||
};
|
||||
let cdb = cmd.encode_cdb();
|
||||
|
||||
let mut pdu = IscsiPdu::new(Opcode::ScsiCmd);
|
||||
pdu.lun = lun;
|
||||
pdu.set_data(bytes::Bytes::from(cdb));
|
||||
|
||||
conn.send_pdu(&pdu).await.ok();
|
||||
conn.recv_pdu().await.ok();
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let ops_per_sec = iterations as f64 / elapsed.as_secs_f64();
|
||||
|
||||
println!("Performance Results:");
|
||||
println!(" Operations: {}", iterations);
|
||||
println!(" Time: {:.2}s", elapsed.as_secs_f64());
|
||||
println!(" Ops/sec: {:.2}", ops_per_sec);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Login error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
conn.close().await.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Connection error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
rust-iscsi-initiator/src/connection/mod.rs
Normal file
127
rust-iscsi-initiator/src/connection/mod.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use crate::Result;
|
||||
use crate::pdu::IscsiPdu;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
/// iSCSI Connection
|
||||
pub struct IscsiConnection {
|
||||
/// TCP stream
|
||||
stream: TcpStream,
|
||||
|
||||
/// Session ID
|
||||
session_id: u64,
|
||||
|
||||
/// Command Sequence Number
|
||||
cmd_sn: u32,
|
||||
|
||||
/// Status Sequence Number
|
||||
stat_sn: u32,
|
||||
}
|
||||
|
||||
impl IscsiConnection {
|
||||
/// Create new connection to iSCSI target
|
||||
pub async fn connect(addr: &str) -> Result<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
|
||||
Ok(Self {
|
||||
stream,
|
||||
session_id: 0,
|
||||
cmd_sn: 0,
|
||||
stat_sn: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Send PDU to target
|
||||
pub async fn send_pdu(&mut self, pdu: &IscsiPdu) -> Result<()> {
|
||||
let data = pdu.encode();
|
||||
self.stream.write_all(&data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive PDU from target
|
||||
pub async fn recv_pdu(&mut self) -> Result<IscsiPdu> {
|
||||
// Read header (48 bytes)
|
||||
let mut header = [0u8; 48];
|
||||
self.stream.read_exact(&mut header).await?;
|
||||
|
||||
// Parse data segment length
|
||||
let data_len =
|
||||
((header[5] as usize) << 16) | ((header[6] as usize) << 8) | (header[7] as usize);
|
||||
|
||||
// Read data segment if present
|
||||
if data_len > 0 {
|
||||
let mut full_pdu = Vec::with_capacity(48 + data_len);
|
||||
full_pdu.extend_from_slice(&header);
|
||||
|
||||
let mut data = vec![0u8; data_len];
|
||||
self.stream.read_exact(&mut data).await?;
|
||||
full_pdu.extend(data);
|
||||
|
||||
IscsiPdu::decode(&full_pdu)
|
||||
} else {
|
||||
IscsiPdu::decode(&header)
|
||||
}
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Perform iSCSI login
|
||||
pub async fn login(&mut self, initiator_name: &str, target_name: &str) -> Result<()> {
|
||||
let login_pdu = IscsiPdu::login_request(initiator_name, target_name);
|
||||
self.send_pdu(&login_pdu).await?;
|
||||
|
||||
let response = self.recv_pdu().await?;
|
||||
|
||||
// Check login response
|
||||
if response.opcode != 0x23 {
|
||||
return Err(crate::Error::Protocol("Invalid login response".into()));
|
||||
}
|
||||
|
||||
// Parse login parameters
|
||||
if response.data.len() > 0 {
|
||||
let params = String::from_utf8_lossy(&response.data);
|
||||
log::info!("Login response: {}", params);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send NOP-Out (keepalive)
|
||||
pub async fn nop_out(&mut self) -> Result<()> {
|
||||
let pdu = IscsiPdu::nop_out();
|
||||
self.send_pdu(&pdu).await?;
|
||||
|
||||
let response = self.recv_pdu().await?;
|
||||
|
||||
if response.opcode != 0x20 {
|
||||
return Err(crate::Error::Protocol("Invalid NOP-In response".into()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close connection
|
||||
pub async fn close(&mut self) -> Result<()> {
|
||||
self.stream.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get next command sequence number
|
||||
pub fn next_cmd_sn(&mut self) -> u32 {
|
||||
let sn = self.cmd_sn;
|
||||
self.cmd_sn += 1;
|
||||
sn
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_mock() {
|
||||
// Mock test - would need real iSCSI target for full test
|
||||
let result = IscsiConnection::connect("127.0.0.1:3260").await;
|
||||
// Should fail if no target running
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
}
|
||||
}
|
||||
108
rust-iscsi-initiator/src/crc32c.rs
Normal file
108
rust-iscsi-initiator/src/crc32c.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use crc32c::crc32c_append;
|
||||
|
||||
/// CRC32C checksum implementation for iSCSI
|
||||
pub struct Crc32c {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl Crc32c {
|
||||
/// Create new CRC32C calculator
|
||||
pub fn new() -> Self {
|
||||
Self { value: 0 }
|
||||
}
|
||||
|
||||
/// Initialize with existing value
|
||||
pub fn with_value(value: u32) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
|
||||
/// Append data to CRC calculation
|
||||
pub fn append(&mut self, data: &[u8]) {
|
||||
self.value = crc32c_append(self.value, data);
|
||||
}
|
||||
|
||||
/// Get final CRC value
|
||||
pub fn finalize(&self) -> u32 {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Calculate CRC32C for entire buffer
|
||||
pub fn calculate(data: &[u8]) -> u32 {
|
||||
crc32c::crc32c(data)
|
||||
}
|
||||
|
||||
/// Verify CRC32C checksum
|
||||
pub fn verify(data: &[u8], expected: u32) -> bool {
|
||||
Self::calculate(data) == expected
|
||||
}
|
||||
|
||||
/// Convert to 4-byte array (little-endian)
|
||||
pub fn to_bytes(&self) -> [u8; 4] {
|
||||
self.value.to_le_bytes()
|
||||
}
|
||||
|
||||
/// Convert to 4-byte array (big-endian, iSCSI standard)
|
||||
pub fn to_bytes_be(&self) -> [u8; 4] {
|
||||
self.value.to_be_bytes()
|
||||
}
|
||||
|
||||
/// Parse from 4-byte array (big-endian)
|
||||
pub fn from_bytes_be(bytes: [u8; 4]) -> Self {
|
||||
Self {
|
||||
value: u32::from_be_bytes(bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Crc32c {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_crc32c_basic() {
|
||||
let data = b"Hello, World!";
|
||||
let crc = Crc32c::calculate(data);
|
||||
|
||||
// Should match known CRC32C value
|
||||
assert!(crc != 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crc32c_append() {
|
||||
let mut crc = Crc32c::new();
|
||||
crc.append(b"Hello");
|
||||
crc.append(b"World");
|
||||
|
||||
let final_crc = crc.finalize();
|
||||
|
||||
let direct_crc = Crc32c::calculate(b"HelloWorld");
|
||||
|
||||
assert_eq!(final_crc, direct_crc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crc32c_verify() {
|
||||
let data = b"test data";
|
||||
let crc = Crc32c::calculate(data);
|
||||
|
||||
assert!(Crc32c::verify(data, crc));
|
||||
assert!(!Crc32c::verify(data, crc + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crc32c_bytes() {
|
||||
let crc = Crc32c::with_value(0x12345678);
|
||||
|
||||
let bytes_be = crc.to_bytes_be();
|
||||
assert_eq!(bytes_be, [0x12, 0x34, 0x56, 0x78]);
|
||||
|
||||
let parsed = Crc32c::from_bytes_be(bytes_be);
|
||||
assert_eq!(parsed.value, 0x12345678);
|
||||
}
|
||||
}
|
||||
109
rust-iscsi-initiator/src/discovery/mod.rs
Normal file
109
rust-iscsi-initiator/src/discovery/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use crate::connection::IscsiConnection;
|
||||
use crate::pdu::{IscsiPdu, Opcode};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
/// Target discovery
|
||||
pub struct Discovery {
|
||||
connection: Option<IscsiConnection>,
|
||||
}
|
||||
|
||||
impl Discovery {
|
||||
/// Create new discovery instance
|
||||
pub fn new() -> Self {
|
||||
Self { connection: None }
|
||||
}
|
||||
|
||||
/// Connect to discovery portal
|
||||
pub async fn connect(&mut self, addr: &str) -> Result<()> {
|
||||
let conn = IscsiConnection::connect(addr).await?;
|
||||
self.connection = Some(conn);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send SendTargets discovery request
|
||||
pub async fn send_targets(&mut self) -> Result<Vec<String>> {
|
||||
if let Some(conn) = &mut self.connection {
|
||||
// Create Text request with SendTargets
|
||||
let mut pdu = IscsiPdu::new(Opcode::TextCmd);
|
||||
pdu.set_data(bytes::Bytes::from("SendTargets=All\n"));
|
||||
|
||||
conn.send_pdu(&pdu).await?;
|
||||
|
||||
let response = conn.recv_pdu().await?;
|
||||
|
||||
// Parse SendTargets response
|
||||
if response.data.len() > 0 {
|
||||
let targets_str = String::from_utf8_lossy(&response.data);
|
||||
let targets = targets_str
|
||||
.lines()
|
||||
.filter(|line| line.starts_with("TargetName="))
|
||||
.map(|line| line.trim_start_matches("TargetName=").trim().to_string())
|
||||
.collect();
|
||||
|
||||
return Ok(targets);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Disconnect from discovery portal
|
||||
pub async fn disconnect(&mut self) -> Result<()> {
|
||||
if let Some(conn) = &mut self.connection {
|
||||
conn.close().await?;
|
||||
self.connection = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovery result
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TargetInfo {
|
||||
/// Target name (IQN)
|
||||
pub target_name: String,
|
||||
|
||||
/// Portal addresses
|
||||
pub portals: Vec<String>,
|
||||
|
||||
/// LUNs available
|
||||
pub luns: Vec<u64>,
|
||||
}
|
||||
|
||||
impl TargetInfo {
|
||||
/// Create new target info
|
||||
pub fn new(target_name: String) -> Self {
|
||||
Self {
|
||||
target_name,
|
||||
portals: Vec::new(),
|
||||
luns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add portal
|
||||
pub fn add_portal(&mut self, portal: String) {
|
||||
self.portals.push(portal);
|
||||
}
|
||||
|
||||
/// Add LUN
|
||||
pub fn add_lun(&mut self, lun: u64) {
|
||||
self.luns.push(lun);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_target_info() {
|
||||
let mut info = TargetInfo::new("iqn.test".to_string());
|
||||
info.add_portal("127.0.0.1:3260".to_string());
|
||||
info.add_lun(0);
|
||||
|
||||
assert_eq!(info.target_name, "iqn.test");
|
||||
assert_eq!(info.portals.len(), 1);
|
||||
assert_eq!(info.luns.len(), 1);
|
||||
}
|
||||
}
|
||||
50
rust-iscsi-initiator/src/lib.rs
Normal file
50
rust-iscsi-initiator/src/lib.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
pub mod connection;
|
||||
pub mod crc32c;
|
||||
pub mod discovery;
|
||||
pub mod login;
|
||||
pub mod pdu;
|
||||
pub mod scsi;
|
||||
pub mod tools;
|
||||
|
||||
pub use connection::IscsiConnection;
|
||||
pub use pdu::IscsiPdu;
|
||||
pub use scsi::ScsiCommand;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Protocol error: {0}")]
|
||||
Protocol(String),
|
||||
|
||||
#[error("SCSI error: {0}")]
|
||||
Scsi(String),
|
||||
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
#[error("PDU parse error: {0}")]
|
||||
PduParse(String),
|
||||
}
|
||||
|
||||
/// iSCSI Initiator main entry point
|
||||
pub struct Initiator {
|
||||
connections: Vec<connection::IscsiConnection>,
|
||||
}
|
||||
|
||||
impl Initiator {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
connections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self, addr: &str) -> Result<()> {
|
||||
let conn = connection::IscsiConnection::connect(addr).await?;
|
||||
self.connections.push(conn);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
195
rust-iscsi-initiator/src/login/mod.rs
Normal file
195
rust-iscsi-initiator/src/login/mod.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::Result;
|
||||
use crate::connection::IscsiConnection;
|
||||
use crate::pdu::{IscsiPdu, Opcode};
|
||||
|
||||
/// iSCSI Login parameters
|
||||
pub struct LoginParams {
|
||||
/// Initiator name (IQN)
|
||||
initiator_name: String,
|
||||
|
||||
/// Target name (IQN)
|
||||
target_name: String,
|
||||
|
||||
/// Session type (Discovery/Normal)
|
||||
session_type: SessionType,
|
||||
|
||||
/// Authentication method
|
||||
auth_method: AuthMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SessionType {
|
||||
/// Discovery session
|
||||
Discovery,
|
||||
|
||||
/// Normal session
|
||||
Normal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AuthMethod {
|
||||
/// No authentication
|
||||
None,
|
||||
|
||||
/// CHAP authentication
|
||||
Chap { username: String, password: String },
|
||||
}
|
||||
|
||||
impl LoginParams {
|
||||
/// Create new login parameters
|
||||
pub fn new(initiator_name: String, target_name: String) -> Self {
|
||||
Self {
|
||||
initiator_name,
|
||||
target_name,
|
||||
session_type: SessionType::Normal,
|
||||
auth_method: AuthMethod::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set session type
|
||||
pub fn set_session_type(&mut self, session_type: SessionType) {
|
||||
self.session_type = session_type;
|
||||
}
|
||||
|
||||
/// Set CHAP authentication
|
||||
pub fn set_chap_auth(&mut self, username: String, password: String) {
|
||||
self.auth_method = AuthMethod::Chap { username, password };
|
||||
}
|
||||
|
||||
/// Encode as iSCSI text parameters
|
||||
pub fn encode(&self) -> String {
|
||||
let mut params = String::new();
|
||||
|
||||
params.push_str(&format!("InitiatorName={}\n", self.initiator_name));
|
||||
params.push_str(&format!("TargetName={}\n", self.target_name));
|
||||
|
||||
match self.session_type {
|
||||
SessionType::Discovery => params.push_str("SessionType=Discovery\n"),
|
||||
SessionType::Normal => params.push_str("SessionType=Normal\n"),
|
||||
}
|
||||
|
||||
match &self.auth_method {
|
||||
AuthMethod::None => params.push_str("AuthMethod=None\n"),
|
||||
AuthMethod::Chap { .. } => params.push_str("AuthMethod=CHAP\n"),
|
||||
}
|
||||
|
||||
// Add default parameters
|
||||
params.push_str("InitialR2T=Yes\n");
|
||||
params.push_str("ImmediateData=Yes\n");
|
||||
params.push_str("MaxRecvDataSegmentLength=65536\n");
|
||||
|
||||
params
|
||||
}
|
||||
}
|
||||
|
||||
/// Login response parser
|
||||
pub struct LoginResponse {
|
||||
/// Status class
|
||||
status_class: u8,
|
||||
|
||||
/// Status detail
|
||||
status_detail: u8,
|
||||
|
||||
/// Response parameters
|
||||
params: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl LoginResponse {
|
||||
/// Parse login response from bytes
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
let text = String::from_utf8_lossy(data);
|
||||
let mut params = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
if line.contains('=') {
|
||||
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||
if parts.len() == 2 {
|
||||
params.push((parts[0].to_string(), parts[1].to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract status
|
||||
let status_class = params
|
||||
.iter()
|
||||
.find(|(k, _)| k == "StatusClass")
|
||||
.and_then(|(_, v)| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let status_detail = params
|
||||
.iter()
|
||||
.find(|(k, _)| k == "StatusDetail")
|
||||
.and_then(|(_, v)| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Self {
|
||||
status_class,
|
||||
status_detail,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if login successful
|
||||
pub fn is_success(&self) -> bool {
|
||||
self.status_class == 0
|
||||
}
|
||||
|
||||
/// Get parameter value
|
||||
pub fn get_param(&self, key: &str) -> Option<&str> {
|
||||
self.params
|
||||
.iter()
|
||||
.find(|(k, _)| k == key)
|
||||
.map(|(_, v)| v.as_str())
|
||||
}
|
||||
|
||||
/// Get all parameters
|
||||
pub fn get_params(&self) -> &Vec<(String, String)> {
|
||||
&self.params
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended login method for connection
|
||||
pub async fn login_with_params(
|
||||
conn: &mut IscsiConnection,
|
||||
params: &LoginParams,
|
||||
) -> Result<LoginResponse> {
|
||||
// Create login PDU
|
||||
let mut pdu = IscsiPdu::new(Opcode::LoginCmd);
|
||||
pdu.flags = 0x80; // Transit bit
|
||||
pdu.set_data(bytes::Bytes::from(params.encode()));
|
||||
|
||||
// Send login request
|
||||
conn.send_pdu(&pdu).await?;
|
||||
|
||||
// Receive login response
|
||||
let response = conn.recv_pdu().await?;
|
||||
|
||||
// Parse response
|
||||
LoginResponse::parse(&response.data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_login_params() {
|
||||
let params = LoginParams::new("iqn.initiator".to_string(), "iqn.target".to_string());
|
||||
let encoded = params.encode();
|
||||
|
||||
assert!(encoded.contains("InitiatorName=iqn.initiator"));
|
||||
assert!(encoded.contains("TargetName=iqn.target"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_response_parse() {
|
||||
let data = "StatusClass=0\nStatusDetail=0\nMaxRecvDataSegmentLength=65536\n";
|
||||
let response = LoginResponse::parse(data.as_bytes()).unwrap();
|
||||
|
||||
assert!(response.is_success());
|
||||
assert_eq!(
|
||||
response.get_param("MaxRecvDataSegmentLength"),
|
||||
Some("65536")
|
||||
);
|
||||
}
|
||||
}
|
||||
251
rust-iscsi-initiator/src/pdu/mod.rs
Normal file
251
rust-iscsi-initiator/src/pdu/mod.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use std::io;
|
||||
|
||||
/// iSCSI PDU (Protocol Data Unit)
|
||||
///
|
||||
/// Based on RFC 3720: iSCSI Basic Header Format
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IscsiPdu {
|
||||
/// Opcode (1 byte)
|
||||
pub opcode: u8,
|
||||
|
||||
/// Flags (1 byte)
|
||||
pub flags: u8,
|
||||
|
||||
/// Reserved (2 bytes)
|
||||
pub reserved: [u8; 2],
|
||||
|
||||
/// Total AHS Length (1 byte)
|
||||
pub total_ahs_len: u8,
|
||||
|
||||
/// Data Segment Length (3 bytes)
|
||||
pub data_segment_len: [u8; 3],
|
||||
|
||||
/// Logical Unit Number (8 bytes)
|
||||
pub lun: u64,
|
||||
|
||||
/// Initiator Task Tag (4 bytes)
|
||||
pub itt: u32,
|
||||
|
||||
/// Target Task Tag (4 bytes)
|
||||
pub ttt: u32,
|
||||
|
||||
/// Command Sequence Number (4 bytes)
|
||||
pub cmd_sn: u32,
|
||||
|
||||
/// Expected Status Sequence Number (4 bytes)
|
||||
pub exp_stat_sn: u32,
|
||||
|
||||
/// Expected Command Sequence Number (4 bytes)
|
||||
pub exp_cmd_sn: u32,
|
||||
|
||||
/// Maximum Command Sequence Number (4 bytes)
|
||||
pub max_cmd_sn: u32,
|
||||
|
||||
/// Data payload
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
/// iSCSI Opcode types
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Opcode {
|
||||
/// SCSI Command
|
||||
ScsiCmd = 0x01,
|
||||
|
||||
/// SCSI Response
|
||||
ScsiResp = 0x21,
|
||||
|
||||
/// Login Command
|
||||
LoginCmd = 0x03,
|
||||
|
||||
/// Login Response
|
||||
LoginResp = 0x23,
|
||||
|
||||
/// Logout Command
|
||||
LogoutCmd = 0x06,
|
||||
|
||||
/// Logout Response
|
||||
LogoutResp = 0x26,
|
||||
|
||||
/// NOP-Out
|
||||
NopOut = 0x00,
|
||||
|
||||
/// NOP-In
|
||||
NopIn = 0x20,
|
||||
|
||||
/// Text Command
|
||||
TextCmd = 0x04,
|
||||
|
||||
/// Text Response
|
||||
TextResp = 0x24,
|
||||
}
|
||||
|
||||
impl IscsiPdu {
|
||||
/// Create a new PDU with given opcode
|
||||
pub fn new(opcode: Opcode) -> Self {
|
||||
Self {
|
||||
opcode: opcode as u8,
|
||||
flags: 0,
|
||||
reserved: [0, 0],
|
||||
total_ahs_len: 0,
|
||||
data_segment_len: [0, 0, 0],
|
||||
lun: 0,
|
||||
itt: 0,
|
||||
ttt: 0xFFFFFFFF, // Reserved value
|
||||
cmd_sn: 0,
|
||||
exp_stat_sn: 0,
|
||||
exp_cmd_sn: 0,
|
||||
max_cmd_sn: 0,
|
||||
data: Bytes::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode PDU to bytes
|
||||
pub fn encode(&self) -> Bytes {
|
||||
let total_len = 48 + self.data.len();
|
||||
let mut buf = BytesMut::with_capacity(total_len);
|
||||
|
||||
// Basic Header Segment (BHS) - 48 bytes
|
||||
buf.put_u8(self.opcode);
|
||||
buf.put_u8(self.flags);
|
||||
buf.put_slice(&self.reserved);
|
||||
buf.put_u8(self.total_ahs_len);
|
||||
buf.put_slice(&self.data_segment_len);
|
||||
buf.put_u64_le(self.lun);
|
||||
buf.put_u32_le(self.itt);
|
||||
buf.put_u32_le(self.ttt);
|
||||
buf.put_u32_le(self.cmd_sn);
|
||||
buf.put_u32_le(self.exp_stat_sn);
|
||||
buf.put_u32_le(self.exp_cmd_sn);
|
||||
buf.put_u32_le(self.max_cmd_sn);
|
||||
|
||||
// Reserved fields (8 bytes to complete 48-byte header)
|
||||
buf.put_bytes(0, 8);
|
||||
|
||||
// Data segment
|
||||
buf.put_slice(&self.data);
|
||||
|
||||
buf.freeze()
|
||||
}
|
||||
|
||||
/// Decode PDU from bytes
|
||||
pub fn decode(src: &[u8]) -> io::Result<Self> {
|
||||
if src.len() < 48 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"PDU header too short",
|
||||
));
|
||||
}
|
||||
|
||||
let opcode = src[0];
|
||||
let flags = src[1];
|
||||
let reserved = [src[2], src[3]];
|
||||
let total_ahs_len = src[4];
|
||||
let data_segment_len = [src[5], src[6], src[7]];
|
||||
|
||||
// Read LUN (8 bytes)
|
||||
let lun = u64::from_le_bytes([
|
||||
src[8], src[9], src[10], src[11], src[12], src[13], src[14], src[15],
|
||||
]);
|
||||
|
||||
// Read ITT (4 bytes)
|
||||
let itt = u32::from_le_bytes([src[16], src[17], src[18], src[19]]);
|
||||
|
||||
// Read TTT (4 bytes)
|
||||
let ttt = u32::from_le_bytes([src[20], src[21], src[22], src[23]]);
|
||||
|
||||
// Read CmdSN (4 bytes)
|
||||
let cmd_sn = u32::from_le_bytes([src[24], src[25], src[26], src[27]]);
|
||||
|
||||
// Read ExpStatSN (4 bytes)
|
||||
let exp_stat_sn = u32::from_le_bytes([src[28], src[29], src[30], src[31]]);
|
||||
|
||||
// Read ExpCmdSN (4 bytes)
|
||||
let exp_cmd_sn = u32::from_le_bytes([src[32], src[33], src[34], src[35]]);
|
||||
|
||||
// Read MaxCmdSN (4 bytes)
|
||||
let max_cmd_sn = u32::from_le_bytes([src[36], src[37], src[38], src[39]]);
|
||||
|
||||
// Extract data segment
|
||||
let data_len = Self::parse_data_segment_len(&data_segment_len);
|
||||
let data = if data_len > 0 && src.len() >= 48 + data_len {
|
||||
Bytes::copy_from_slice(&src[48..48 + data_len])
|
||||
} else {
|
||||
Bytes::new()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
opcode,
|
||||
flags,
|
||||
reserved,
|
||||
total_ahs_len,
|
||||
data_segment_len,
|
||||
lun,
|
||||
itt,
|
||||
ttt,
|
||||
cmd_sn,
|
||||
exp_stat_sn,
|
||||
exp_cmd_sn,
|
||||
max_cmd_sn,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse 3-byte data segment length
|
||||
fn parse_data_segment_len(len: &[u8; 3]) -> usize {
|
||||
((len[0] as usize) << 16) | ((len[1] as usize) << 8) | (len[2] as usize)
|
||||
}
|
||||
|
||||
/// Set data payload
|
||||
pub fn set_data(&mut self, data: Bytes) {
|
||||
self.data = data;
|
||||
let len = self.data.len();
|
||||
self.data_segment_len = [(len >> 16) as u8, (len >> 8) as u8, len as u8];
|
||||
}
|
||||
|
||||
/// Create Login PDU
|
||||
pub fn login_request(initiator_name: &str, target_name: &str) -> Self {
|
||||
let mut pdu = Self::new(Opcode::LoginCmd);
|
||||
|
||||
// Login parameters as text
|
||||
let params = format!(
|
||||
"InitiatorName={}\nTargetName={}\n",
|
||||
initiator_name, target_name
|
||||
);
|
||||
pdu.set_data(Bytes::from(params));
|
||||
|
||||
pdu
|
||||
}
|
||||
|
||||
/// Create NOP-Out PDU (keepalive)
|
||||
pub fn nop_out() -> Self {
|
||||
Self::new(Opcode::NopOut)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pdu_encode_decode() {
|
||||
let mut pdu = IscsiPdu::new(Opcode::ScsiCmd);
|
||||
pdu.lun = 1;
|
||||
pdu.itt = 42;
|
||||
|
||||
let encoded = pdu.encode();
|
||||
assert_eq!(encoded.len(), 48);
|
||||
|
||||
let decoded = IscsiPdu::decode(&encoded).unwrap();
|
||||
assert_eq!(decoded.opcode, Opcode::ScsiCmd as u8);
|
||||
assert_eq!(decoded.lun, 1);
|
||||
assert_eq!(decoded.itt, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_pdu() {
|
||||
let pdu = IscsiPdu::login_request("iqn.test", "iqn.target");
|
||||
assert_eq!(pdu.opcode, Opcode::LoginCmd as u8);
|
||||
assert!(pdu.data.len() > 0);
|
||||
}
|
||||
}
|
||||
545
rust-iscsi-initiator/src/scsi/mod.rs
Normal file
545
rust-iscsi-initiator/src/scsi/mod.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
use crate::Result;
|
||||
|
||||
/// SCSI Command abstraction
|
||||
pub enum ScsiCommand {
|
||||
/// Test Unit Ready
|
||||
TestUnitReady,
|
||||
|
||||
/// Inquiry
|
||||
Inquiry,
|
||||
|
||||
/// Read (6 bytes CDB)
|
||||
Read6 { lba: u32, transfer_length: u8 },
|
||||
|
||||
/// Read (10 bytes CDB)
|
||||
Read10 { lba: u32, transfer_length: u16 },
|
||||
|
||||
/// Read (16 bytes CDB)
|
||||
Read16 { lba: u64, transfer_length: u32 },
|
||||
|
||||
/// Write (6 bytes CDB)
|
||||
Write6 { lba: u32, transfer_length: u8 },
|
||||
|
||||
/// Write (10 bytes CDB)
|
||||
Write10 { lba: u32, transfer_length: u16 },
|
||||
|
||||
/// Write (16 bytes CDB)
|
||||
Write16 { lba: u64, transfer_length: u32 },
|
||||
|
||||
/// Read Capacity (10)
|
||||
ReadCapacity10,
|
||||
|
||||
/// Read Capacity (16)
|
||||
ReadCapacity16,
|
||||
|
||||
/// Mode Sense (6)
|
||||
ModeSense6 {
|
||||
page_code: u8,
|
||||
allocation_length: u8,
|
||||
},
|
||||
|
||||
/// Mode Sense (10)
|
||||
ModeSense10 {
|
||||
page_code: u8,
|
||||
allocation_length: u16,
|
||||
},
|
||||
|
||||
/// Mode Select (6)
|
||||
ModeSelect6 {
|
||||
page_code: u8,
|
||||
parameter_list: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Mode Select (10)
|
||||
ModeSelect10 {
|
||||
page_code: u8,
|
||||
parameter_list: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Unmap
|
||||
Unmap { lba: u64, blocks: u32 },
|
||||
|
||||
/// Write Same (10)
|
||||
WriteSame10 { lba: u32, blocks: u16 },
|
||||
|
||||
/// Write Same (16)
|
||||
WriteSame16 { lba: u64, blocks: u32 },
|
||||
|
||||
/// Persistent Reserve In
|
||||
PersistentReserveIn {
|
||||
service_action: u8,
|
||||
scope: u8,
|
||||
key: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Persistent Reserve Out
|
||||
PersistentReserveOut {
|
||||
service_action: u8,
|
||||
scope: u8,
|
||||
key: Vec<u8>,
|
||||
},
|
||||
|
||||
/// Start Stop Unit
|
||||
StartStopUnit { start: bool, load_eject: bool },
|
||||
|
||||
/// Prevent Allow Medium Removal
|
||||
PreventAllowMediumRemoval { prevent: bool },
|
||||
|
||||
/// Synchronize Cache (10)
|
||||
SynchronizeCache10 { lba: u32, blocks: u16 },
|
||||
|
||||
/// Synchronize Cache (16)
|
||||
SynchronizeCache16 { lba: u64, blocks: u32 },
|
||||
|
||||
/// Verify (10)
|
||||
Verify10 { lba: u32, blocks: u16 },
|
||||
|
||||
/// Verify (16)
|
||||
Verify16 { lba: u64, blocks: u32 },
|
||||
}
|
||||
|
||||
impl ScsiCommand {
|
||||
/// Encode SCSI CDB (Command Descriptor Block)
|
||||
pub fn encode_cdb(&self) -> Vec<u8> {
|
||||
match self {
|
||||
ScsiCommand::TestUnitReady => {
|
||||
vec![0x00, 0, 0, 0, 0, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::Inquiry => {
|
||||
vec![0x12, 0, 0, 0, 0x24, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::Read6 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
vec![
|
||||
0x08,
|
||||
((lba >> 16) & 0x1F) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
*transfer_length,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::Read10 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
vec![
|
||||
0x28,
|
||||
0,
|
||||
((lba >> 24) & 0xFF) as u8,
|
||||
((lba >> 16) & 0xFF) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
0,
|
||||
((transfer_length >> 8) & 0xFF) as u8,
|
||||
(transfer_length & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::Read16 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
let mut cdb = vec![0x88, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&transfer_length.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::Write6 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
vec![
|
||||
0x0A,
|
||||
((lba >> 16) & 0x1F) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
*transfer_length,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::Write10 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
vec![
|
||||
0x2A,
|
||||
0,
|
||||
((lba >> 24) & 0xFF) as u8,
|
||||
((lba >> 16) & 0xFF) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
0,
|
||||
((transfer_length >> 8) & 0xFF) as u8,
|
||||
(transfer_length & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::Write16 {
|
||||
lba,
|
||||
transfer_length,
|
||||
} => {
|
||||
let mut cdb = vec![0x8A, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&transfer_length.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::ReadCapacity10 => {
|
||||
vec![0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::ReadCapacity16 => {
|
||||
vec![0x9E, 0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x20, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::ModeSense6 {
|
||||
page_code,
|
||||
allocation_length,
|
||||
} => {
|
||||
vec![0x1A, 0, *page_code, 0, *allocation_length, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::ModeSense10 {
|
||||
page_code,
|
||||
allocation_length,
|
||||
} => {
|
||||
vec![
|
||||
0x5A,
|
||||
0,
|
||||
0,
|
||||
*page_code,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
((allocation_length >> 8) & 0xFF) as u8,
|
||||
(allocation_length & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::ModeSelect6 {
|
||||
page_code,
|
||||
parameter_list,
|
||||
} => {
|
||||
let mut cdb = vec![0x15, 0, *page_code, 0, parameter_list.len() as u8, 0];
|
||||
cdb.extend(parameter_list);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::ModeSelect10 {
|
||||
page_code,
|
||||
parameter_list,
|
||||
} => {
|
||||
let mut cdb = vec![
|
||||
0x55,
|
||||
0,
|
||||
0,
|
||||
*page_code,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
((parameter_list.len() >> 8) & 0xFF) as u8,
|
||||
(parameter_list.len() & 0xFF) as u8,
|
||||
0,
|
||||
];
|
||||
cdb.extend(parameter_list);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::Unmap { lba, blocks } => {
|
||||
let mut cdb = vec![0x42, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&blocks.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::WriteSame10 { lba, blocks } => {
|
||||
vec![
|
||||
0x41,
|
||||
0,
|
||||
((lba >> 24) & 0xFF) as u8,
|
||||
((lba >> 16) & 0xFF) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
0,
|
||||
((blocks >> 8) & 0xFF) as u8,
|
||||
(blocks & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::WriteSame16 { lba, blocks } => {
|
||||
let mut cdb = vec![0x93, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&blocks.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::PersistentReserveIn {
|
||||
service_action,
|
||||
scope,
|
||||
key,
|
||||
} => {
|
||||
let mut cdb = vec![
|
||||
0x5E,
|
||||
(service_action & 0x1F) | ((scope << 4) & 0xF0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
((key.len() >> 8) & 0xFF) as u8,
|
||||
(key.len() & 0xFF) as u8,
|
||||
0,
|
||||
];
|
||||
cdb.extend(key);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::PersistentReserveOut {
|
||||
service_action,
|
||||
scope,
|
||||
key,
|
||||
} => {
|
||||
let mut cdb = vec![
|
||||
0x5F,
|
||||
(service_action & 0x1F) | ((scope << 4) & 0xF0),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
((key.len() >> 8) & 0xFF) as u8,
|
||||
(key.len() & 0xFF) as u8,
|
||||
0,
|
||||
];
|
||||
cdb.extend(key);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::StartStopUnit { start, load_eject } => {
|
||||
vec![
|
||||
0x1B,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
if *start { 1 } else { 0 } | if *load_eject { 2 } else { 0 },
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::PreventAllowMediumRemoval { prevent } => {
|
||||
vec![0x1E, 0, 0, 0, if *prevent { 1 } else { 0 }, 0]
|
||||
}
|
||||
|
||||
ScsiCommand::SynchronizeCache10 { lba, blocks } => {
|
||||
vec![
|
||||
0x35,
|
||||
0,
|
||||
((lba >> 24) & 0xFF) as u8,
|
||||
((lba >> 16) & 0xFF) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
0,
|
||||
((blocks >> 8) & 0xFF) as u8,
|
||||
(blocks & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::SynchronizeCache16 { lba, blocks } => {
|
||||
let mut cdb = vec![0x91, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&blocks.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
|
||||
ScsiCommand::Verify10 { lba, blocks } => {
|
||||
vec![
|
||||
0x2F,
|
||||
0,
|
||||
((lba >> 24) & 0xFF) as u8,
|
||||
((lba >> 16) & 0xFF) as u8,
|
||||
((lba >> 8) & 0xFF) as u8,
|
||||
(lba & 0xFF) as u8,
|
||||
0,
|
||||
((blocks >> 8) & 0xFF) as u8,
|
||||
(blocks & 0xFF) as u8,
|
||||
0,
|
||||
]
|
||||
}
|
||||
|
||||
ScsiCommand::Verify16 { lba, blocks } => {
|
||||
let mut cdb = vec![0x8F, 0];
|
||||
cdb.extend_from_slice(&lba.to_be_bytes());
|
||||
cdb.extend_from_slice(&blocks.to_be_bytes());
|
||||
cdb.push(0);
|
||||
cdb.push(0);
|
||||
cdb
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get command name
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
ScsiCommand::TestUnitReady => "Test Unit Ready",
|
||||
ScsiCommand::Inquiry => "Inquiry",
|
||||
ScsiCommand::Read6 { .. } => "Read(6)",
|
||||
ScsiCommand::Read10 { .. } => "Read(10)",
|
||||
ScsiCommand::Read16 { .. } => "Read(16)",
|
||||
ScsiCommand::Write6 { .. } => "Write(6)",
|
||||
ScsiCommand::Write10 { .. } => "Write(10)",
|
||||
ScsiCommand::Write16 { .. } => "Write(16)",
|
||||
ScsiCommand::ReadCapacity10 => "Read Capacity(10)",
|
||||
ScsiCommand::ReadCapacity16 => "Read Capacity(16)",
|
||||
ScsiCommand::ModeSense6 { .. } => "Mode Sense(6)",
|
||||
ScsiCommand::ModeSense10 { .. } => "Mode Sense(10)",
|
||||
ScsiCommand::ModeSelect6 { .. } => "Mode Select(6)",
|
||||
ScsiCommand::ModeSelect10 { .. } => "Mode Select(10)",
|
||||
ScsiCommand::Unmap { .. } => "Unmap",
|
||||
ScsiCommand::WriteSame10 { .. } => "Write Same(10)",
|
||||
ScsiCommand::WriteSame16 { .. } => "Write Same(16)",
|
||||
ScsiCommand::PersistentReserveIn { .. } => "Persistent Reserve In",
|
||||
ScsiCommand::PersistentReserveOut { .. } => "Persistent Reserve Out",
|
||||
ScsiCommand::StartStopUnit { .. } => "Start Stop Unit",
|
||||
ScsiCommand::PreventAllowMediumRemoval { .. } => "Prevent/Allow Medium Removal",
|
||||
ScsiCommand::SynchronizeCache10 { .. } => "Synchronize Cache(10)",
|
||||
ScsiCommand::SynchronizeCache16 { .. } => "Synchronize Cache(16)",
|
||||
ScsiCommand::Verify10 { .. } => "Verify(10)",
|
||||
ScsiCommand::Verify16 { .. } => "Verify(16)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inquiry response structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InquiryResponse {
|
||||
/// Peripheral device type
|
||||
pub peripheral_type: u8,
|
||||
|
||||
/// Vendor identification
|
||||
pub vendor_id: String,
|
||||
|
||||
/// Product identification
|
||||
pub product_id: String,
|
||||
|
||||
/// Product revision level
|
||||
pub product_rev: String,
|
||||
}
|
||||
|
||||
impl InquiryResponse {
|
||||
/// Parse inquiry response from bytes
|
||||
pub fn parse(data: &[u8]) -> Result<Self> {
|
||||
if data.len() < 36 {
|
||||
return Err(crate::Error::Scsi("Inquiry data too short".into()));
|
||||
}
|
||||
|
||||
let peripheral_type = data[0] & 0x1F;
|
||||
let vendor_id = String::from_utf8_lossy(&data[8..16]).trim().to_string();
|
||||
let product_id = String::from_utf8_lossy(&data[16..32]).trim().to_string();
|
||||
let product_rev = String::from_utf8_lossy(&data[32..36]).trim().to_string();
|
||||
|
||||
Ok(Self {
|
||||
peripheral_type,
|
||||
vendor_id,
|
||||
product_id,
|
||||
product_rev,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Capacity response structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadCapacityResponse {
|
||||
/// Maximum LBA
|
||||
pub max_lba: u64,
|
||||
|
||||
/// Block size
|
||||
pub block_size: u32,
|
||||
}
|
||||
|
||||
impl ReadCapacityResponse {
|
||||
/// Parse from bytes
|
||||
pub fn parse_10(data: &[u8]) -> Result<Self> {
|
||||
if data.len() < 8 {
|
||||
return Err(crate::Error::Scsi("Read Capacity data too short".into()));
|
||||
}
|
||||
|
||||
let max_lba = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as u64;
|
||||
let block_size = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
|
||||
|
||||
Ok(Self {
|
||||
max_lba,
|
||||
block_size,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse from 16-byte response
|
||||
pub fn parse_16(data: &[u8]) -> Result<Self> {
|
||||
if data.len() < 32 {
|
||||
return Err(crate::Error::Scsi(
|
||||
"Read Capacity(16) data too short".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let max_lba = u64::from_be_bytes([
|
||||
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
|
||||
]);
|
||||
let block_size = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
|
||||
|
||||
Ok(Self {
|
||||
max_lba,
|
||||
block_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cdb_encode() {
|
||||
let cmd = ScsiCommand::TestUnitReady;
|
||||
let cdb = cmd.encode_cdb();
|
||||
assert_eq!(cdb.len(), 6);
|
||||
assert_eq!(cdb[0], 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_capacity() {
|
||||
let data = [0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x02, 0x00];
|
||||
let resp = ReadCapacityResponse::parse_10(&data).unwrap();
|
||||
assert_eq!(resp.max_lba, 127);
|
||||
assert_eq!(resp.block_size, 512);
|
||||
}
|
||||
}
|
||||
35
rust-iscsi-initiator/src/tools/common.rs
Normal file
35
rust-iscsi-initiator/src/tools/common.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::connection::IscsiConnection;
|
||||
use crate::pdu::{IscsiPdu, Opcode};
|
||||
use crate::scsi::{InquiryResponse, ReadCapacityResponse, ScsiCommand};
|
||||
|
||||
/// Common tool utilities
|
||||
pub async fn inquiry(conn: &mut IscsiConnection, lun: u64) -> crate::Result<InquiryResponse> {
|
||||
let cmd = ScsiCommand::Inquiry;
|
||||
let cdb = cmd.encode_cdb();
|
||||
|
||||
let mut pdu = IscsiPdu::new(Opcode::ScsiCmd);
|
||||
pdu.lun = lun;
|
||||
pdu.set_data(bytes::Bytes::from(cdb));
|
||||
|
||||
conn.send_pdu(&pdu).await?;
|
||||
let response = conn.recv_pdu().await?;
|
||||
|
||||
InquiryResponse::parse(&response.data)
|
||||
}
|
||||
|
||||
pub async fn read_capacity(
|
||||
conn: &mut IscsiConnection,
|
||||
lun: u64,
|
||||
) -> crate::Result<ReadCapacityResponse> {
|
||||
let cmd = ScsiCommand::ReadCapacity10;
|
||||
let cdb = cmd.encode_cdb();
|
||||
|
||||
let mut pdu = IscsiPdu::new(Opcode::ScsiCmd);
|
||||
pdu.lun = lun;
|
||||
pdu.set_data(bytes::Bytes::from(cdb));
|
||||
|
||||
conn.send_pdu(&pdu).await?;
|
||||
let response = conn.recv_pdu().await?;
|
||||
|
||||
ReadCapacityResponse::parse_10(&response.data)
|
||||
}
|
||||
6
rust-iscsi-initiator/src/tools/mod.rs
Normal file
6
rust-iscsi-initiator/src/tools/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Placeholder for tools module
|
||||
// Tool implementations are in src/bin/
|
||||
|
||||
pub mod common;
|
||||
|
||||
pub use common::*;
|
||||
Reference in New Issue
Block a user