Copilot/ChatGPT의 도움으로 NVMe-oF 패킷으로부터 헤더를 추출하여 원하는 커맨드 핸들러로 분기하는 코드를 작성해 보았다. 그렇다면, 이제는 커맨드를 하나씩 구현해 볼 차례이다. 일단 쉬워보이는 Identify를 골라, ChatGPT에게 차근차근 물어보도록 하자:

How to implement Identify command handler for NVMe-oF protocol in RUST?

ChatGPT: Implementing an Identify command handler for NVMe-oF protocol in Rust involves a few steps. …

struct Identify {
    stream: TcpStream,
    header_vec: Vec<u8>,
}

impl Identify {
    fn new(stream: TcpStream, header: &[u8]) -> Identify {
        Identify {
            stream,
            header_vec: Vec::from(header),
        }
    }

    pub async fn handle(&mut self) -> Result<(), Box<dyn Error>> {
        let (_opcode, identify_params) = self.parse_command().unwrap();
        let cns = identify_params[0];
        let csi = u32::from_le_bytes(identify_params[1..5].try_into().unwrap());
        let nvmsetid = u32::from_le_bytes(identify_params[5..9].try_into().unwrap());
        let uuidindex = u32::from_le_bytes(identify_params[9..13].try_into().unwrap());
        let uuidlist = &identify_params[13..];
        self.populate_identify_controller(cns, csi, nvmsetid, uuidindex, uuidlist);

        let mut response = Vec::new();
        response.push(0); // status code
        response.push(0); // status code specific
        response.push(0); // reserved
        response.push(0); // reserved
        response.push(0); // reserved
        response.push(0); // reserved
        response.push(0); // reserved
        response.push(0); // reserved

        self.stream.write(response.as_slice());

        Ok(())
    }

    fn parse_command(&self) -> Result<(u8, &[u8]), String> {
        let command = self.header_vec.as_slice();

        // Check that the command has at least 4 bytes
        if command.len() < 4 {
            return Err("Command buffer too small".to_owned());
        }

        // Extract the opcode and parameter length fields
        let opcode = command[0];
        let plen = u16::from_le_bytes(command[2..4].try_into().unwrap()) as usize;

        // Check that the command buffer is large enough to contain the parameters
        if command.len() < plen + 4 {
            return Err("Command buffer too small".to_owned());
        }

        // Return the opcode and parameters
        Ok((opcode, &command[4..(plen + 4)]))
    }

    fn populate_identify_controller(&self, cns: u8, csi: u32, nvmsetid: u32, uuidindex: u32, uuidlist: &[u8]) -> Vec<u8> {
        match cns {
            NVME_CTRLR_IDENTIFY_CNS_CTRLR => {
                let controller = IdentifyController {
                    vendor_id: 0x1234,
                    subsystem_vendor_id: 0x5678,
                    serial_number: "SN1234".to_owned(),
                    model_number: "Model1234".to_owned(),
                    firmware_revision: "1.0".to_owned(),
                    recommended_aen_bitmap: 0x01,
                    controller_multi_path_io_and_namespace_attach: 0,
                    maximum_data_transfer_size: 0x4000,
                    controller_id: 1,
                };
                bincode::serialize(&controller).unwrap()
            }
            NVME_CTRLR_IDENTIFY_CNS_NS => {
                // TODO: implement namespace information based on csi, uuidindex, and uuidlist
                let ns_data = vec![0; 4096];
                let mut response = vec![0; 4096 + 4];
                byteorder::LittleEndian::write_u32(&mut response[0..4], 4096);
                response[4..].copy_from_slice(&ns_data);
                response
            }
            _ => {
                let response = vec![0; 4];
                byteorder::LittleEndian::write_u32(&mut response[..4], 0xffffffff);
                response
            }
        }
    }
}

ChatGPT로부터 뼈대를 받아와, Copilot으로 완성한 코드는 위와 같았다. 간단히 요약하면, NVMe-oF 패킷의 헤더로부터 Identify 커맨드를 위한 파라미터를 추출해야 한다. ChatGPT가 써준 parse_command에 따르면, 파라미터의 경우 고정된 길이가 아니고 가변길이로 디자인되어 있기 때문에, command[2..4]에 있는 plen이라는 값을 추출해서, command[4..(plen + 4)])를 통해 파라미터들을 얻어온다. 그 다음에는, NVMe controller의 binary encoding을 수행하여, 이를 TcpStream를 통해 해당 구조체를 호스트에게 보내준다. vendor_id, subsystem_vendor_id, serial_number, model_number, firmware_revision, recommended_aen_bitmap, controller_multi_path_io_and_namespace_attach, maximum_data_transfer_size, controller_id들은 현재는 임의의 값을 채워넣었지만, 추후 cns, csi, nvmsetid, uuidindex, uuidlist를 활용하여 의미있는 로직을 추가할 수 있을 것이다.

코드는 작성하였으나, 아직 테스트가 없다. nvme-cli를 사용하면 쉽게 NVMe-oF 커맨드를 만들어서 보내는 방식으로 system test를 작성할 수 있을텐데, 문제는 리눅스 전용툴인 관계로 Mac에서는 사용할 수 없다는 점이 단점이고, 따라서 별도의 가상화 솔루션을 사용해야 한다. 이에 대해서는 다음 포스팅에서 이어가겠다. 본 블로그 작성은 예제 포함 30분+ 정도 소요되었다.