🗄️

cephのpool dump (rados export) をparseする

に公開

What

Object storageであるCephはRADOS Gateway(rgw)経由でS3互換のAPIを提供しているが、S3を経由にせずpool dump自体parseできないかを試した。

rados export -p default.rgw.buckets.index rados_export.bin
#                                         ↑これをparseしたい

TL;DR

C++の実装読んだらなんとかなった。結構面白いフォーマットだった。
https://github.com/yue4u/ceph-rados-export-parser

プロセス

まずhexとにらめっこしてmagic headerに気づく。

ceph magic

cephをcloneして調べる。

const uint16_t shortmagic = 0xffce;	//goes into stream as "ceff"
//endmagic goes into stream as "ceff ffec"
const mymagic_t endmagic = (0xecff << 16) | shortmagic;

https://github.com/ceph/ceph/blob/4adaf64/src/tools/RadosDump.h#L47

headerとfooterのencodeを読み解く。

struct super_header {
  static const uint32_t super_magic = (shortmagic << 16) | shortmagic;
  // ver = 1, Initial version
  // ver = 2, Add OSDSuperblock to pg_begin
  static const uint32_t super_ver = 2;
  static const uint32_t FIXED_LENGTH = 16;
  uint32_t magic;
  uint32_t version;
  uint32_t header_size;
  uint32_t footer_size;

  super_header() : magic(0), version(0), header_size(0), footer_size(0) { }

  void encode(bufferlist& bl) const {
    using ceph::encode;
    encode(magic, bl);
    encode(version, bl);
    encode(header_size, bl);
    encode(footer_size, bl);
  }
  void decode(bufferlist::const_iterator& bl) {
    using ceph::decode;
    decode(magic, bl);
    decode(version, bl);
    decode(header_size, bl);
    decode(footer_size, bl);
  }
};

struct header {
  sectiontype_t type;
  mysize_t size;
  header(sectiontype_t type, mysize_t size) :
    type(type), size(size) { }
  header(): type(0), size(0) { }

  void encode(bufferlist& bl) const {
    uint32_t debug_type = (type << 24) | (type << 16) | shortmagic;
    ENCODE_START(1, 1, bl);
    encode(debug_type, bl);
    encode(size, bl);
    ENCODE_FINISH(bl);
  }
  void decode(bufferlist::const_iterator& bl) {
    uint32_t debug_type;
    DECODE_START(1, bl);
    decode(debug_type, bl);
    type = debug_type >> 24;
    decode(size, bl);
    DECODE_FINISH(bl);
  }
};

struct footer {
  mymagic_t magic;
  footer() : magic(endmagic) { }

  void encode(bufferlist& bl) const {
    ENCODE_START(1, 1, bl);
    encode(magic, bl);
    ENCODE_FINISH(bl);
  }
  void decode(bufferlist::const_iterator& bl) {
    DECODE_START(1, bl);
    decode(magic, bl);
    DECODE_FINISH(bl);
  }
};

https://github.com/ceph/ceph/blob/4adaf64/src/tools/RadosDump.h#L57-L108

ceph::encodeを確認する -> src/include/encoding.h。

基本uint8に変換してbufferlistにそのまま書き込んでいる。

template<class T>
inline void encode_raw(const T& t, bufferlist& bl)
{
  bl.append((char*)&t, sizeof(t));
}
template<class T>
inline void decode_raw(T& t, bufferlist::const_iterator &p)
{
  p.copy(sizeof(t), (char*)&t);
}

// -----------------------------------
// int types

#define WRITE_INTTYPE_ENCODER(type, etype)				\\
  inline void encode(type v, ::ceph::bufferlist& bl, uint64_t features=0) { \\
    ceph_##etype e;					                \\
    e = v;                                                              \\
    ::ceph::encode_raw(e, bl);						\\
  }									\\
  inline void decode(type &v, ::ceph::bufferlist::const_iterator& p) {	\\
    ceph_##etype e;							\\
    ::ceph::decode_raw(e, p);						\\
    v = e;								\\
  }

WRITE_INTTYPE_ENCODER(uint64_t, le64)
WRITE_INTTYPE_ENCODER(int64_t, le64)
WRITE_INTTYPE_ENCODER(uint32_t, le32)
WRITE_INTTYPE_ENCODER(int32_t, le32)
WRITE_INTTYPE_ENCODER(uint16_t, le16)
WRITE_INTTYPE_ENCODER(int16_t, le16)

src/tools/rados/PoolDump.cc側の実装を確認する。

int PoolDump::dump(IoCtx *io_ctx)
{
  ceph_assert(io_ctx != NULL);

  int r = 0;
  // 1️⃣
  write_super();

  // 2️⃣
  r = write_simple(TYPE_POOL_BEGIN, file_fd);

  librados::NObjectIterator i = io_ctx->nobjects_begin();

  for (; i != i_end; ++i) {
    // 3️⃣
    r = write_section(TYPE_OBJECT_BEGIN, obj_begin, file_fd);

    while (true) {
      r = io_ctx->read(oid, outdata, op_size, offset);
      if (r <= 0) {
        // Error or no data
        break;
      }
      // 4️⃣
      r = write_section(TYPE_DATA,
          data_section(offset, outdata.length(), outdata), file_fd);
    }

    // 5️⃣
    r = write_section(TYPE_ATTRS, attr_section(xattrs), file_fd);
    // 6️⃣
    r = write_section(TYPE_OMAP_HDR, omap_hdr_section(omap_header), file_fd);

    do {
      // 7️⃣
      r = write_section(TYPE_OMAP, omap_section(values), file_fd);
    } while (r == MAX_READ);

    // Close object
    // =============
    // 8️⃣
    r = write_simple(TYPE_OBJECT_END, file_fd);
  }

  // 9️⃣
  r = write_simple(TYPE_POOL_END, file_fd);
  return r;
}

write_* callは9箇所しかなく、write_super/write_simple/write_sectionの三種類しかなかった。

write_simpleとwrite_sectionを再度確認する。

    // Define this in .h because it's templated
    template <typename T>
      int write_section(sectiontype_t type, const T& obj, int fd) {
        if (dry_run)
          return 0;
        bufferlist blhdr, bl, blftr;
        obj.encode(bl);
        header hdr(type, bl.length());
        hdr.encode(blhdr);
        footer ft;
        ft.encode(blftr);

        int ret = blhdr.write_fd(fd);
        if (ret) return ret;
        ret = bl.write_fd(fd);
        if (ret) return ret;
        ret = blftr.write_fd(fd);
        return ret;
      }

    int write_simple(sectiontype_t type, int fd)
    {
      if (dry_run)
        return 0;
      bufferlist hbl;

      header hdr(type, 0);
      hdr.encode(hbl);
      return hbl.write_fd(fd);
    }

write_simpleはheader onlyで、write_sectionは header + 任意のobject + footerの形式のようだ。headerのencode処理を照らし合わせると、dumpデータのHex状態から以下の内容が見える。

enum {
    TYPE_NONE = 0,
    TYPE_PG_BEGIN, // 1
    TYPE_PG_END, // 2
    TYPE_OBJECT_BEGIN, // 3
    TYPE_OBJECT_END, // 4
    TYPE_DATA, // 5
    TYPE_ATTRS, // 6
    TYPE_OMAP_HDR, // 7
    TYPE_OMAP, // 8
    TYPE_PG_METADATA, // 9
    TYPE_POOL_BEGIN, // 10
    TYPE_POOL_END, // 11
    END_OF_TYPES,	//Keep at the end // 12
};

type tag

ここまで来たら、レイアウト自体parseできるようになった。

※ 以下は仮実装

//! DEMO decoding impl of ceph `rados export -p default.rgw.buckets.index rados_export.bin`
//! NOTE: error handling reduced for simplicity
use std::io::{Cursor, Read};

fn main() {
    let data = include_bytes!("../data/rados_export.bin");
    let mut cursor = Cursor::new(data.as_slice());

    /*
       - 16bytes super header
       - loop
         - 18bytes block header (simple) (type_tag + size)
         - optional section
           - $size bytes block content
             - std::map<std::string, cepth::bufferlist> if type_tag == TYPE_OMAP
                 - std::string = object key
                 - meta
           - 10bytes footer
    */
    read_super(&mut cursor);
    loop {
        let (tag, size) = read_simple(&mut cursor);
        match tag {
            TypeTag::TYPE_NONE
            | TypeTag::TYPE_PG_BEGIN
            | TypeTag::TYPE_PG_END
            | TypeTag::TYPE_OBJECT_BEGIN
            | TypeTag::TYPE_DATA
            | TypeTag::TYPE_ATTRS
            | TypeTag::TYPE_OMAP_HDR
            | TypeTag::TYPE_PG_METADATA
            | TypeTag::END_OF_TYPES => {
                read_section(&mut cursor, size);
            }
            TypeTag::TYPE_OMAP => {
                let omap_buf = read_section(&mut cursor, size);
                let mut omap_buf_cursor = Cursor::new(omap_buf);
                read_omap(&mut omap_buf_cursor);
            }
            TypeTag::TYPE_POOL_BEGIN | TypeTag::TYPE_OBJECT_END => {}
            TypeTag::TYPE_POOL_END => {
                break;
            }
        }
    }
}

#[allow(non_camel_case_types)]
#[derive(Debug)]
enum TypeTag {
    TYPE_NONE = 0,
    TYPE_PG_BEGIN,     // 1
    TYPE_PG_END,       // 2
    TYPE_OBJECT_BEGIN, // 3
    TYPE_OBJECT_END,   // 4
    TYPE_DATA,         // 5
    TYPE_ATTRS,        // 6
    TYPE_OMAP_HDR,     // 7
    TYPE_OMAP,         // 8
    TYPE_PG_METADATA,  // 9
    TYPE_POOL_BEGIN,   // 10
    TYPE_POOL_END,     // 11
    END_OF_TYPES,      // Keep at the end
}

impl From<u8> for TypeTag {
    fn from(value: u8) -> Self {
        match value {
            0 => Self::TYPE_NONE,
            1 => Self::TYPE_PG_BEGIN,
            2 => Self::TYPE_PG_END,
            3 => Self::TYPE_OBJECT_BEGIN,
            4 => Self::TYPE_OBJECT_END,
            5 => Self::TYPE_DATA,
            6 => Self::TYPE_ATTRS,
            7 => Self::TYPE_OMAP_HDR,
            8 => Self::TYPE_OMAP,
            9 => Self::TYPE_PG_METADATA,
            10 => Self::TYPE_POOL_BEGIN,
            11 => Self::TYPE_POOL_END,
            12 => Self::END_OF_TYPES,
            _ => unreachable!(),
        }
    }
}

impl From<&[u8; 4]> for TypeTag {
    fn from(value: &[u8; 4]) -> Self {
        ((u32_from_le_slice(value) >> 24) as u8).into()
    }
}

impl AsRef<str> for TypeTag {
    fn as_ref(&self) -> &str {
        match self {
            TypeTag::TYPE_NONE => "TYPE_NONE",
            TypeTag::TYPE_PG_BEGIN => "TYPE_PG_BEGIN",
            TypeTag::TYPE_PG_END => "TYPE_PG_END",
            TypeTag::TYPE_OBJECT_BEGIN => "TYPE_OBJECT_BEGIN",
            TypeTag::TYPE_OBJECT_END => "TYPE_OBJECT_END",
            TypeTag::TYPE_DATA => "TYPE_DATA",
            TypeTag::TYPE_ATTRS => "TYPE_ATTRS",
            TypeTag::TYPE_OMAP_HDR => "TYPE_OMAP_HDR",
            TypeTag::TYPE_OMAP => "TYPE_OMAP",
            TypeTag::TYPE_PG_METADATA => "TYPE_PG_METADATA",
            TypeTag::TYPE_POOL_BEGIN => "TYPE_POOL_BEGIN",
            TypeTag::TYPE_POOL_END => "TYPE_POOL_END",
            TypeTag::END_OF_TYPES => "END_OF_TYPES",
        }
    }
}

// 16 bytes
fn read_super(cursor: &mut Cursor<&[u8]>) {
    let mut header = [0; 16];
    _ = cursor.read_exact(&mut header);

    let magic = &header[..4];
    let version = u32_from_le_slice(&header[4..8]);
    let header_len = u32_from_le_slice(&header[8..12]);
    let footer_len = u32_from_le_slice(&header[12..16]);
    println!(
        "{:X?} version={:?} header_len={:?} footer_len={:?}",
        &magic, version, header_len, footer_len
    );
}

// 6 bytes
fn read_meta(cursor: &mut Cursor<&[u8]>) -> i32 {
    let mut starting = [0; 1 + 1]; // v + compact_v
    let mut struct_len = [0; 4];
    _ = cursor.read_exact(&mut starting);
    _ = cursor.read_exact(&mut struct_len);
    i32_from_le_slice(&struct_len)
}

// 18 bytes
fn read_simple(cursor: &mut Cursor<&[u8]>) -> (TypeTag, usize) {
    read_meta(cursor);
    read_header(cursor)
}

// 12 bytes
fn read_header(cursor: &mut Cursor<&[u8]>) -> (TypeTag, usize) {
    let mut section_type = [0; 4];
    _ = cursor.read_exact(&mut section_type);

    let section_type_t = TypeTag::from(&section_type);
    println!("{}", section_type_t.as_ref());

    let mut size_buf = [0; 8];
    _ = cursor.read_exact(&mut size_buf);

    let mysize_t = i64_from_le_slice(&size_buf) as usize;
    // println!("header size={mysize_t}");
    (section_type_t, mysize_t)
}

// 10 bytes
fn read_footer(cursor: &mut Cursor<&[u8]>) {
    read_meta(cursor);
    let mut magic = [0; 4];
    _ = cursor.read_exact(&mut magic);
    assert_eq!(magic, [0xCE, 0xFF, 0xFF, 0xEC])
}

fn read_section<'a>(cursor: &mut Cursor<&'a [u8]>, header_size: usize) -> &'a [u8] {
    let buf = read_n_bytes(cursor, header_size);

    if header_size > 0 {
        read_footer(cursor);
    }
    buf
}

read_headerの実装は省くが、基本の考え方はgrpc/protobufに近い。append onlyとし、structのバージョンとheader内でencoding長を定義することで、後方互換性を維持しながら未知のフィールドをスキップできる。

header

body

ここから肝心なTYPE_OMAPをparseする。

struct omap_section {
  std::map<std::string, bufferlist> omap;
  explicit omap_section(const std::map<std::string, bufferlist> &omap) :
    omap(omap) { }
  omap_section() { }

  void encode(bufferlist& bl) const {
    ENCODE_START(1, 1, bl);
    encode(omap, bl);
    ENCODE_FINISH(bl);
  }
  void decode(bufferlist::const_iterator& bl) {
    DECODE_START(1, bl);
    decode(omap, bl);
    DECODE_FINISH(bl);
  }
};

std::mapに対応するencoding形式を調べたところ、map_sizeをencodeしてからkeyとvalueをlist化しているだけなので簡単だった。

// map
template<class T, class U, class Comp, class Alloc,
	 typename t_traits, typename u_traits>
inline std::enable_if_t<!t_traits::supported ||
			!u_traits::supported>
  encode(const std::map<T,U,Comp,Alloc>& m, bufferlist& bl)
{
  __u32 n = (__u32)(m.size());
  encode(n, bl);
  for (auto p = m.begin(); p != m.end(); ++p) {
    encode(p->first, bl);
    encode(p->second, bl);
  }
}

対応実装

fn read_omap(cursor: &mut Cursor<&[u8]>) {
    read_meta(cursor);

    let map_size = read_u32(cursor);
    println!("map_size: {map_size}");

    for _ in 0..map_size {
        let entry = read_string(cursor);
        // meta
        let buf_len = read_u32(cursor);
        let bytes = read_n_bytes(cursor, buf_len as usize);
        let mut bytes_cursor = Cursor::new(bytes);

        // 未知なfields、多分rgw_bucket_dir_entry_meta
        _ = read_n_bytes(&mut bytes_cursor, 6);
        _ = read_string(&mut bytes_cursor);
        _ = read_n_bytes(&mut bytes_cursor, 32);

        _ = read_string(&mut bytes_cursor);
        let owner = read_string(&mut bytes_cursor);

        // ...
    }
}

注意が必要なのは、ここの string は std::string であり、utf8 を保証していないことだ。実際、試した限りでは Ceph の object version を使っているケースのみ string に 0x00 が混ざっていたため、それ以上の深追いはしなかった。それ以外に触れたデータ範囲内では、utf8 として扱っても特に問題はなかった。

type omap

string

let utf8_bytes = vec![
    0xE3, 0x81, 0x93, 0xE3, 0x82, 0x93, 
    0xE3, 0x81, 0xAB, 0xE3, 0x81, 0xA1, 
    0xE3, 0x81, 0xAF, 0x2E, 0x6D, 0x64
];

println!("{:?}", String::from_utf8(utf8_bytes));
// Ok("こんにちは.md")

フォーマット概要は、以下のようになっている。

    - 16bytes super header
    - loop
      - 18bytes block header (simple) (type_tag + size)
      - optional section
        - $size bytes block content
          - std::map<std::string, cepth::bufferlist> if type_tag == TYPE_OMAP
              - std::string = object key
              - meta
        - 10bytes footer

ここまですべてのomapのownerとentryをparseできた。おわり。

Appendix.1

RadosImport.cc をビルドできれば少し変えるだけで動きそうだったが、あんまり楽ではなかった。(特にBoost C++系の環境セットアップに時間かかりそうな気がした)

Appendix.2

librados のpython packageもあるが、OS限定だったり今回の目的に向いてなかった。

参考

Discussion